A routing engine in Rust, API-compatible with openrouteservice, with public transport as a first-class citizen rather than a bolted-on module. openrouteservice inspired this project and its REST contract is the compatibility target; none of its code is used here — see NOTICE.
Compatibility is a property of the HTTP boundary only. The REST contract — paths, request fields, response fields, error codes — matches ORS so existing clients work unchanged. Everything behind it is chosen for speed and memory footprint: memory-mapped columnar graphs, cost arrays baked at build time, and RAPTOR for transit.
| Phase | State |
|---|---|
| 0 — scaffold, error contract, config, swagger | done |
| 1 — OSM import, street graph, directions endpoint | done for driving-car and foot-walking |
2 — GTFS import, RAPTOR, public-transport |
done |
| 3 — matrix, isochrones, snap, export | done, including additive transit variants |
| 4–6 — profiles/options, performance, client/delivery | planned |
Measured against upstream openrouteservice 9.10.0 on the same Heidelberg extract, same machine:
| Open Reroute | openrouteservice 9.10.0 | |
|---|---|---|
| p50 directions (1.3 km, car) | 0.49 ms | 2.24 ms |
| p95 | 0.82 ms | 3.03 ms |
| Resident memory, 2 profiles | 17 MB | 743 MB |
| Graph build | 0.1 s | minutes |
| Cold start with a prebuilt graph | 13 ms | minutes |
orscompat reports 41/41 structural matches: 25 directions cases plus 16 matrix, isochrone, snap
and export cases, including error codes. Four isochrone area samples are within 0.2% of the same
openrouteservice 9.10.0 reference.
Bulk endpoints on a Stuttgart-sized graph: a 50×50 street matrix answers in 21 ms, a 15×15 transit matrix in 1.2 s, and a 900-second car isochrone in 11 ms (upstream: 30 ms).
Car durations are calibrated against upstream: median −1.2 % across 24 varied routes, with distances at +0.1 %. See docs/06-cost-models.md for the model and the measurements.
Public transport, built from the official VVS feed over a Stuttgart extract:
| Feed | 242,517 trips · 9,768 stops · 743 routes · 365 days of calendar |
| Timetable build | 8.2 s (whole graph + timetable: 11 s) |
| Timetable artifact | 48.6 MB, memory-mapped |
| Journey query | Stuttgart Hbf → Vaihingen, S1 direct, answered in single-digit ms |
Build a graph from an OSM extract:
cargo run --release -p or-cli -- build --source path/to/extract.osm.pbf --out data/graph.orrt --profiles driving-car,foot-walking,cycling-regularAdd a GTFS feed to serve public transport as well — the timetable is written next to the graph as
data/graph.orrt.transit and picked up automatically when serving:
cargo run --release -p or-cli -- build --source path/to/extract.osm.pbf --out data/graph.orrt --profiles driving-car,foot-walking,cycling-regular --gtfs path/to/feed.zipServe the API:
cargo run --release -p or-cli -- serve --graph data/graph.orrt --port 8082Ask for a route:
curl -X POST http://localhost:8082/ors/v2/directions/driving-car/json -H 'Content-Type: application/json' -d '{"coordinates":[[8.681495,49.41461],[8.687872,49.420318]]}'Ask for a journey by public transport:
curl -X POST http://localhost:8082/ors/v2/directions/public-transport/json -H 'Content-Type: application/json' -d '{"coordinates":[[9.180229,48.783386],[9.1130,48.7250]],"departure":"2026-10-15T09:00:00"}'Interactive docs are at http://localhost:8082/ors/swagger-ui, the OpenAPI document at
/ors/v2/api-docs. The base path defaults to /ors, matching the upstream Docker image; set
server.base_path to / to serve at the root like the public API.
Start the reference instance (upstream ORS on a Heidelberg extract, capped at 2 CPUs and 3 GB):
docker compose -f deploy/reference-ors/docker-compose.yml up -dThen replay the corpus against both:
cargo run --release -p or-compat -- --ours http://localhost:8082/ors --reference http://localhost:8081/ors --verboseThe harness fails on structural differences — field names, types, error codes — and only reports distance and duration deltas, because the cost model is deliberately not upstream's.
Stop the reference when you are done:
docker compose -f deploy/reference-ors/docker-compose.yml down| Tool | Version | Why |
|---|---|---|
| Rust | 1.82+ (rustup) |
building everything |
| Docker + Compose | any recent | reference openrouteservice, Postgres/PostGIS |
| An OSM extract | .osm.pbf |
the street network |
| A GTFS feed | .zip |
public transport (phase 2) |
No system libraries are required — no PROJ, no GEOS, no JVM. cargo build is the whole toolchain.
Small extracts to develop against, both shipped in the upstream openrouteservice repository:
ls ../openrouteservice/ors-api/src/test/files/heidelberg.test.pbf ../openrouteservice/ors-api/src/test/files/vrn_gtfs_cut.zipcargo build --releasecargo run --release -p or-cli -- build --source path/to/extract.osm.pbf --out data/graph.orrt --profiles driving-car,foot-walking,cycling-regularcargo run --release -p or-cli -- serve --graph data/graph.orrt --port 8082cargo testcargo clippy --all-targets -- -D warnings && cargo fmt --all --checkorctl --help lists every flag; orctl build --help and orctl serve --help cover the two
subcommands.
Values are layered: defaults, then a YAML file, then ORS_* environment variables, then CLI flags.
The file is optional — the defaults run a local instance as-is.
# ors-config.yml
server:
host: 0.0.0.0
port: 8082
base_path: /ors # "/" serves at the root, like the public ORS API
engine:
graph_path: data/graph.orrt
source_file: path/to/extract.osm.pbf
profiles: [driving-car, foot-walking]
limits:
maximum_waypoints: 50
maximum_distance_m: 100000
maximum_snapping_radius_m: 350| Environment variable | Overrides | Default |
|---|---|---|
ORS_CONFIG_LOCATION |
path of the config file | none |
ORS_SERVER_HOST |
server.host |
0.0.0.0 |
ORS_SERVER_PORT |
server.port |
8082 |
ORS_SERVER_BASE_PATH |
server.base_path |
/ors |
ORS_ENGINE_GRAPH_PATH |
engine.graph_path |
data/graph.orrt |
ORS_ENGINE_SOURCE_FILE |
engine.source_file |
none |
ORS_ENGINE_PROFILES |
engine.profiles (comma-separated) |
driving-car,foot-walking |
ORS_LOG_JSON |
structured log output | false |
RUST_LOG |
log filter, e.g. debug,or_graph=trace |
info |
public-transport needs two things a street profile does not: the foot-walking profile in the
same graph (it powers access, egress and transfers) and a GTFS feed passed to orctl build.
cargo run --release -p or-cli -- build --source stuttgart.osm.pbf --out data/stuttgart.orrt --profiles driving-car,foot-walking --gtfs vvs_gtfs.zipThe example used throughout development is Stuttgart: the BBBike Stuttgart extract for the street network and the official VVS feed from opendata-oepnv.de (registration required; gtfs.mfdz.de mirrors it without one).
| Parameter | Meaning | Default |
|---|---|---|
departure |
local departure time, e.g. 2026-10-15T09:00:00 |
now |
walking_time |
ISO-8601 budget for access, egress and transfers | PT15M |
ignore_transfers |
stop preferring fewer changes when arrival times tie | false |
schedule |
return several departures instead of one journey | false |
schedule_duration |
width of the schedule window | PT30M |
schedule_rows |
maximum journeys returned | 3 |
Timetable times are read in the feed's own time zone (from agency.txt), so departure is local
time and every timestamp in the response comes back with that offset.
When a journey looks wrong, check the data before the engine. orctl inspect reports what the
graph and timetable actually contain at a point:
cargo run --release -p or-cli -- inspect --graph data/stuttgart.orrt --point 9.180229,48.783386 --stopsIt prints where the coordinate snaps, how large the connected component around it is, and which stops are reachable on foot with the routes serving them. A stop that is missing here is a data or linking problem, not a routing one — for example a summer closure genuinely removes a line from the calendar, which is why a query in August and the same query in October give different answers.
| URL | What |
|---|---|
http://localhost:8082/ors/v2/directions/driving-car/json |
routing (POST) |
http://localhost:8082/ors/v2/directions/public-transport/json |
transit journeys (POST) |
http://localhost:8082/ors/v2/matrix/{profile} |
duration/distance matrix (POST) |
http://localhost:8082/ors/v2/isochrones/{profile} |
reachability polygons (POST) |
http://localhost:8082/ors/v2/snap/{profile} |
batch graph snapping (POST) |
http://localhost:8082/ors/v2/export/{profile} |
bounded graph export (POST) |
http://localhost:8082/ors/v2/health |
readiness |
http://localhost:8082/ors/v2/status |
loaded profiles and versions |
http://localhost:8082/ors/swagger-ui |
interactive docs |
http://localhost:8082/ors/v2/api-docs |
OpenAPI document |
The API description is generated from the same utoipa-annotated request/response
structs that parse and render traffic in or-api — there is no hand-maintained spec
to drift from the code.
-
Live, while
orctl serveis running:http://localhost:8082/ors/swagger-ui(interactive) orhttp://localhost:8082/ors/v2/api-docs(raw JSON). -
Static, without a server or graph:
orctl openapineeds no config, graph, or timetable — the spec is pure compile-time annotation. Prints JSON to stdout by default:cargo run -p or-cli -- openapi # JSON to stdout cargo run -p or-cli -- openapi --format yaml --out spec.yaml -
Checked-in copies live at
docs/api/openapi.jsonanddocs/api/openapi.yaml, for anything that wants a spec file without building the project — client generators, doc sites, another repo's CI. Regenerate them after touching a handler signature, DTO, or#[utoipa::path]annotation:make openapi # regenerate docs/api/openapi.{json,yaml} make openapi-check # fail if the checked-in copies are stale (wire into CI)
graph has no costs for profile 'x'— the artifact was built without that profile. Rebuild with--profiles, which is what/v2/statuslists.2010 point not found— the coordinate is outside the extract or further from the network than the snapping radius. Passradiuses, or check you are sending[longitude, latitude].2004 exceeds server limit— straight-line extent abovelimits.maximum_distance_m(100 km by default, matching upstream).2013/2014/2015— no stop is reachable on foot from the start, the destination, or either. Raisewalking_time, or check withorctl inspect --stopsthat stops nearby were linked to the street network at build time.2016 route not found— both ends reach the network but nothing connects them within the six-hour search horizon. Usually a calendar question: is the line running on that date?- Port already in use — a previous
orctl serveis still running:pkill -f "orctl serve".
A React + Vite + Leaflet client lives in frontend/, with one screen per
backend capability — Directions (incl. a transit-itinerary breakdown), Isochrones, Matrix,
and a Tools screen bundling Snap and Export. It talks to or-api directly (CORS is
already permissive on the server); nothing about the frontend requires a database or a
second service.
Node 20+ and npm. The backend must be running somewhere reachable (see Quickstart above).
cd frontend
npm install
npm run generate:api-types # generates src/api/schema.ts from docs/api/openapi.json
npm run dev # http://localhost:5173| Variable | Default | What |
|---|---|---|
VITE_API_BASE_URL |
http://localhost:8082/ors |
Base URL of the or-api instance to call |
VITE_TRACESTRACK_API_KEY |
unset | Optional free key from tracestrack.com; enables the "Tracestrack Topo" basemap in the map style switcher. Every other basemap (Street, Satellite, Terrain via OpenTopoMap, Cycle Map via CyclOSM, Transport Map via ÖPNVKarte) needs no key and works out of the box. |
Copy frontend/.env.example to frontend/.env and edit as needed.
Address search in the location fields calls Nominatim
(OpenStreetMap's public geocoder) — the one call the frontend makes to a service other than
or-api, since the backend only accepts coordinates. Clicking the map or typing lat, lon
directly works without it.
frontend/vercel.json is already configured (Vite framework preset, SPA rewrite). Set the
project's Root Directory to frontend and add VITE_API_BASE_URL (and optionally
VITE_TRACESTRACK_API_KEY) as environment variables pointing at wherever or-api is deployed.
npm run dev, npm run build, npm run lint (oxlint), npm test (vitest) — see
frontend/README.md.
crates/ or-domain entities and ports, zero sibling dependencies
or-app use cases, validation, service limits
or-api the only crate that knows ORS field names
or-graph CSR graph, snapping, search, instructions
or-osm PBF import
or-store mmap artifact format
or-geo geometry, polyline, simplification
or-cli orctl: composition root (build / serve)
or-compat orscompat: the compatibility harness
pkg/ ors-errors, ors-config, ors-telemetry cross-cutting, business-free
frontend/ React + Vite + Leaflet client — directions, isochrones, matrix, tools
charts/ open-reroute Helm chart
deploy/ reference-ors: the compatibility oracle
docs/ design documents
See docs/README.md for the design and roadmap.
The backend ships as a container image and a Helm chart, deployed to Kubernetes by GitHub Actions. Full detail — including every repository secret and variable the pipelines need — is in docs/08-deployment.md.
make docker-build # the image CI builds
make deploy-check # chart lint + render + OpenAPI driftThe engine serves from a memory-mapped artifact (190 MB for Stuttgart, 42 MB compressed) that an init container downloads onto a volume, verified by sha256. That digest is also the cache key, so restarts skip the download and refreshing the data is a one-line change rather than an image rebuild.
Apache-2.0. See LICENSE.
open-reRoute is API-compatible with openrouteservice and was inspired by it, but contains none of its code — it is an independent Rust implementation, and compatibility is a property of the HTTP boundary only. NOTICE sets this out in full.
Note that the data is licensed separately from the software: OpenStreetMap is © OpenStreetMap contributors under the ODbL, GTFS feeds under their agencies' own terms, and a routing artifact built from them is a derived database with its own attribution and share-alike obligations.