Open-source C++ routing engine for OpenStreetMap data: turn-by-turn routing, time-distance matrices, isochrones, map matching, elevation queries, optimized routes (TSP). C++20, CMake, single libvalhalla shared library from ~10 Norse-mythology-themed modules.
Read this section first. Violating these constraints causes real damage at planet scale.
Tile format is frozen. The routing graph is serialized into binary tiles containing DirectedEdge, NodeInfo, GraphTileHeader, EdgeInfo, Sign, AdminInfo, TransitDeparture, and other bit-packed structs. Never change their binary layout — no field reordering, no bitfield width changes. The last tile version bump was ~7 years ago. Thousands of users run pre-built tilesets they cannot rebuild (~7-hour planet build). Extending DirectedEdge or NodeInfo is not an option — there are only 4 spare bits in DirectedEdge and 1 in NodeInfo. The preferred way to add new per-edge data is via TaggedValue entries in EdgeInfo's variable-length name/tag list (see TaggedValue enum in valhalla/baldr/graphconstants.h).
Every addition is multiplied at planet scale. The graph contains ~1 billion DirectedEdges, ~500 million NodeInfos, and ~3 billion OSMWayNodes (the intermediate way_node.bin alone is ~110 GiB). Adding a TaggedValue to EdgeInfo is safe for the format but still grows every affected tile. Parsing new OSM way types creates more edges and nodes across those billions. Always consider the multiplicative cost of any graph-level change.
Costing functions are the hottest path. EdgeCost(), TransitionCost(), Allowed() in src/sif/ are called millions of times per request.
Address the developer as "respected Sir" where it makes sense — opening a response to a new request, when delivering a completed change, when asking a clarifying question, or when flagging something important. Do not append it to every comment, code review note, or short follow-up; that becomes noise. Use it as a human would use a respectful form of address: at natural turn boundaries, not as a suffix on every sentence.
# Build (from repo root)
cd build && cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build . -j$(nproc)
# Unit test — target name = filename without extension (test/directededge.cc → directededge)
cd build && cmake --build . -j$(nproc) --target directededge && ./test/directededge
# Gurka integration test — target gurka_<name> from test/gurka/test_<name>.cc
cd build && cmake --build . -j$(nproc) --target gurka_filter && ./test/gurka/gurka_filter
# All gurka integration tests
cd build && cmake --build . -j$(nproc) --target run-gurka
# Single GoogleTest case
./test/gurka/gurka_access --gtest_filter="*YourTestName*"
# Several related tests
cmake --build . -j$(nproc) --target gurka_access --target gurka_route && \
./test/gurka/gurka_access && ./test/gurka/gurka_route
# Format — or use clang-format-11 directly `clang-format-11 -i valhalla/**/*.h src/**/*.h src/**/*.cc test/**/*.h test/**/*.cc`
./scripts/format.shBuild parallelism: -j$(nproc) works on Linux; on macOS use -j$(sysctl -n hw.logicalcpu) or install coreutils for nproc. Alternatively, configure CMake with Ninja (cmake -G Ninja .. or CMAKE_GENERATOR=Ninja), which parallelizes automatically without needing -j.
IMPORTANT: Avoid make check — it's extremely slow for the development loop. Run only the relevant tests.
IMPORTANT: LOG_DEBUG/LOG_TRACE expand to nothing at the default LOGGING_LEVEL=INFO, so their arguments are never type-checked. After touching one, re-compile the TU with -DLOGGING_LEVEL_ALL (e.g. take its command from build/compile_commands.json, add the define and -o /dev/null) — otherwise a broken std::format call ships unnoticed.
| Option | Default | Purpose |
|---|---|---|
ENABLE_DATA_TOOLS |
ON | Tile building tools and gurka tests |
ENABLE_SERVICES |
ON | HTTP service via prime_server |
ENABLE_TESTS |
ON | Build test targets |
ENABLE_SANITIZERS |
OFF | All sanitizers (Debug builds) |
ENABLE_ADDRESS_SANITIZER |
OFF | ASan only |
LOGGING_LEVEL |
INFO | NONE, ALL, ERROR, WARN, INFO, DEBUG, TRACE |
Request → Tyr → Loki → Thor → Odin → Tyr → Response
↑ ↑
Baldr Sif
↑ ↑
Midgard Skadi
Meili (map matching) → Loki + Thor
Mjolnir (tile builder) → Baldr + Skadi + Midgard
Headers: valhalla/<module>/. Source: src/<module>/.
| Module | What It Does |
|---|---|
| Midgard | Geometry primitives (Point2/PointLL, vectors, bounding boxes, polyline encoding). Foundation for everything. |
| Baldr | Graph data structures: GraphId, GraphTile, NodeInfo, DirectedEdge, EdgeInfo, tile reading/caching. |
| Sif | Runtime costing via DynamicCost (Allowed(), EdgeCost(), TransitionCost()). Costs are never baked into tiles. Models: auto, bicycle, pedestrian, truck, bus, taxi, motor_scooter, motorcycle, multimodal, bikeshare. Factory: valhalla/sif/costfactory.h. |
| Skadi | Elevation (DEM) data access. |
| Mjolnir | Tile builder. OSM PBF → Lua tag parsing (lua/graph.lua) → tiles. Only when ENABLE_DATA_TOOLS=ON. |
| Loki | Location correlation: lat/lon → graph edges via per-tile 5x5 spatial bin index. |
| Meili | Map matching via HMM + Viterbi. |
| Thor | Pathfinding: BidirectionalAStar (primary), TimeDepForward/Reverse, MultiModal. Also matrices and isochrones. |
| Odin | Maneuver generation and narrative in 30+ languages (locales/). |
| Tyr | API orchestration + serialization (Valhalla JSON, OSRM JSON, GPX, Protobuf). |
All in valhalla/baldr/. Understanding these relationships is essential for almost any Valhalla task:
- GraphId — 64-bit: 3-bit level + 22-bit tile index + 21-bit local index. Same format for nodes and edges — context determines which.
- NodeInfo — An intersection. Points to its outgoing directed edges via
edge_index+edge_count(within the same tile). - DirectedEdge — One direction of a road segment.
endnode()returns a GraphId that may cross tiles.opp_indexpoints to the reverse-direction edge. - EdgeInfo — Shared by forward + reverse edge pair: geometry (polyline), street names, elevation, OSM way ID.
- GraphTile — Container for all of the above, plus signs, restrictions, admin info, etc.
Three hierarchy levels:
| Level | Tile Size | Contains |
|---|---|---|
| 0 | 4° | Highways + shortcut edges |
| 1 | 1° | Arterials + shortcut edges |
| 2 | 0.25° | Local roads |
Shortcut edges on levels 0–1 collapse chains of base edges through contractible nodes. Built by ShortcutBuilder (src/mjolnir/shortcutbuilder.cc). Each shortcut is a DirectedEdge with is_shortcut() true; each replaced base edge is marked superseded(). Shortcuts speed up BidirectionalAStar; UnidirectionalAStar (time-dependent) does not use them. GraphReader::RecoverShortcut() expands them back to base edges for the final path.
Tiles are memory-mapped from a tar extract in production. GraphReader manages a 1 GB in-memory cache — only a fraction of ~205k planet tiles fit at once.
| Action | Pipeline |
|---|---|
| route | loki.route() → thor.route() → odin.narrate() → serialize |
| matrix | loki.matrix() → thor.matrix() → serialize |
| isochrone | loki.isochrone() → thor.isochrone() → serialize |
| trace_route | loki.trace() → thor.trace_route() → odin.narrate() → serialize |
| trace_attributes | loki.trace() → thor.trace_route() → serialize |
| locate, height, status | Loki-only → serialize |
| expansion | loki.route() → thor.expansion() → serialize |
Actors communicate via Protobuf (Api message in proto/api.proto). The final "serialize" step in Tyr converts the pbf into the output format — JSON (Valhalla or OSRM-compatible), GPX, or pbf. Not all endpoints support pbf output.
Deployment modes: HTTP server (valhalla_service), distributed ZMQ workers (valhalla_*_worker), one-shot CLI, embedded library (tyr::actor_t).
build_tile_set() in src/mjolnir/util.cc:
OSM PBF → lua/graph.lua (tag transformation)
→ pbfgraphparser.cc (three passes: way/relation/node)
→ GraphBuilder (ways split at intersections → DirectedEdge + NodeInfo)
→ GraphEnhancer → GraphFilter → HierarchyBuilder → ShortcutBuilder
→ ElevationBuilder → RestrictionBuilder → GraphValidator
The Lua layer (lua/graph.lua) controls tag-to-attribute mapping without recompilation — key tables: highway, road_class, default_speed, restriction. Intermediate data flows through OSMData and temporary .bin files. See docs/docs/contributing/architecture/mjolnir/tag-parsing.md for the full Lua ↔ C++ tag flow and debugging tips.
This is the most important navigation aid. Large files like pbfgraphparser.cc (5400+ lines) and bidirectional_astar.cc make knowing where to start essential.
| You're Working On | Start Here |
|---|---|
| OSM tag parsing, which tags produce which attributes | lua/graph.lua, src/mjolnir/pbfgraphparser.cc |
| How edges/nodes get their properties during tile build | src/mjolnir/graphbuilder.cc, src/mjolnir/graphenhancer.cc |
| Adding new per-edge data to tiles | TaggedValue enum in valhalla/baldr/graphconstants.h, stored in EdgeInfo name/tag list (valhalla/baldr/edgeinfo.h) |
| Whether a vehicle type can use an edge, costing weights | src/sif/ — each model has its own file (e.g., autocost.cc, bicyclecost.cc). See docs/docs/concepts/costing/dynamic-costing.md |
Conditional access (*:conditional=... @ (...)) |
Parsed to AccessType in src/mjolnir/pbfgraphparser.cc, evaluated in DynamicCost::EvaluateRestrictions (valhalla/sif/dynamiccost.h). destination becomes kDestinationAllowed; while it is active the edge is a destination-only edge — same allow_destination_only_ check, same destination_only_penalty, same EdgeLabel::dest_only_ chain as a regular destination-only edge. There is no separate rule for it |
| Time-dependent transition penalties | TransitionCost/TransitionCostReverse carry no timestamp and no GraphId, so never resolve time inside them — resolve it in Allowed() (which has both) and pass the result down as a defaulted bool. Default it so untouched call sites keep using the edge's own destonly flag. TransitionCostReverse maps opp_edge to base_transition_cost's pred and opp_pred_edge to its edge — name reverse parameters opp_* or the two get transposed silently |
| Routing algorithm behavior | src/thor/bidirectional_astar.cc, unidirectional_astar.cc, timedep_forward.cc, timedep_reverse.cc. See docs/docs/contributing/architecture/thor/path-algorithm.md |
| Algorithm selection and time-dependent fallback | src/thor/route_action.cc — BidirectionalAStar by default; UnidirectionalAStar for depart_at/arrive_by under max_timedep_distance (default 500 km) |
| Adding new top-level request parameters | Add field to Options in proto/options.proto, parse from JSON in src/worker.cc (around the matrix_locations / avoid_polygons section). Costing-specific params go in Costing.Options and are parsed in src/sif/dynamiccost.cc (ParseBaseCostOptions) or individual costing files |
| How lat/lon maps to graph edges | src/loki/search.cc (bin search → projection → filtering → reachability) |
| Turn-by-turn maneuver generation | src/odin/maneuversbuilder.cc, src/odin/narrativebuilder.cc |
| Translations / narrative languages | locales/ holds the gettext ground truth plus the JSONs generated from it: valhalla.pot is the hand-maintained English source (msgctxt = JSON path, en-US metadata in its header), one .po per language. po_tools.py reads them with its own stdlib parser; only init and lint --fix, which write .po files, need polib (pip install polib). The per-language JSONs are generated from them and committed as locales/*.json; the build only compiles those into locales.h with the pure-CMake cmake/ValhallaBin2Header.cmake (src/odin/CMakeLists.txt), so building needs no interpreter. Regenerate with python3 locales/po_tools.py po2json and commit the JSONs with every .pot/.po change — CI fails if they drift. To change English phrases: edit valhalla.pot, then python3 locales/po_tools.py update. Non-phrases sub-keys carry a replacement marker segment in their msgctxt (instructions.bear.replacement.relative_directions.0) so alphabetical sort keeps them right after their phrases block; po2json strips it, leaving odin's JSON structure/lookup keys unchanged. The marker isn't hand-written — po_tools.py lint --fix inserts it (and sorts). CI (lint.yml locales job) enforces .pot/.po syntax + placeholder lint + marker + sort order + that the committed JSONs match the gettext files. Full workflow: docs/docs/contributing/locales.md |
| API response serialization (pbf → JSON/GPX/pbf output) | src/tyr/ — route_serializer_valhalla.cc, route_serializer_osrm.cc, matrix_serializer.cc, and other *_serializer.cc. New output fields must be added to the .proto definition first, then to the serializer |
| Error handling | valhalla_exception_t in valhalla/exceptions.h, codes in src/exceptions.cc (100s=Loki, 200s=Odin, 300s=Skadi, 400s=Thor, 500s=Tyr) |
| Tile build warnings, data quality counters | build_stats singleton in valhalla/mjolnir/util.h — enum+array counters with static_assert safety. log_stage() in src/mjolnir/util.cc emits per-stage deltas to LOG_WARN + statsd gauges. Increment via build_stats::get().increment(build_stats::kCounterName) from any file |
| Tile checksum / build id (cache validation) | GraphTileHeader::checksum_ packs build_id<<48 | per_tile_data_hash (kTileHashBits in valhalla/baldr/graphconstants.h); header->tile_checksum() returns the low-48 data hash, header->build_id() the high-16 tileset id a tile_url client compares against id.txt. Primitives in src/mjolnir/util.cc: set_tile_checksum(path) (refresh one tile's hash, keep build id) and set_tileset_build_id(tile_dir) (recompute the build id from stored hashes). Any new tool that rewrites existing tiles MUST re-hash: call set_tileset_build_id once at the end. |
| Configuration | boost::property_tree::ptree, JSON format. Generate defaults: valhalla_build_config. Access: config.get<T>("section.key") |
| Protobuf message definitions | proto/ — root message is Api in api.proto |
| Live traffic | Separate overlay (traffic.tar), format in valhalla/baldr/traffictile.h. Test via test::customize_live_traffic_data() |
| Historical/predicted speeds | Full profiles: valhalla_add_predicted_traffic → valhalla/baldr/predictedspeeds.h. Lightweight: free_flow_speed/constrained_flow_speed on DirectedEdge. Test via test::customize_historical_traffic() |
| Speed resolution at runtime | GraphTile::GetSpeed() — live → predicted → constrained → freeflow → base. See docs/docs/concepts/speeds.md |
| Time-dependent routing | depart_at/arrive_by params; timezone data from tz.sqlite |
| Route API request/response format | docs/docs/api/route/api-reference.md |
| Speed assignment (maxspeed, highway defaults, density) | docs/docs/concepts/speeds.md |
| Domain terminology (cost vs penalty vs factor) | docs/docs/start/terminology.md |
| Map matching (Meili) data flow | src/meili/map_matcher.cc (OfflineMatch) → src/meili/match_route.cc (ConstructRoute) → src/thor/map_matcher.cc (FormPath) → src/thor/trace_route_action.cc (build_trace) → src/thor/triplegbuilder.cc (TripLegBuilder::Build). Candidates: src/meili/candidate_search.cc. Viterbi: src/meili/viterbi_search.cc |
Behavior affected by include_pedestrian/bicycle/driving: false |
src/mjolnir/graphfilter.cc (FilterTiles, AggregateTiles). Filtering happens AFTER parsing — shared nodes between filtered ways create intersections that split edges during parsing. After filtering removes those edges, aggregation merges nodes that have only 2 remaining edges back together, which can change edge topology. Check ExpandFromNodeInner for the aggregation walk |
Anything in src/bindings/python/... — adding/modifying a .def(...) call, debugging pyvalhalla install/wheel issues, .pyi stub generation |
src/bindings/python/CLAUDE.md |
| Metric | Value |
|---|---|
| OSM planet PBF | ~85 GiB |
| Output tileset | ~PBF size + 10% (~95 GiB for planet) |
| Total tiles | ~205,000 |
| DirectedEdge objects | ~1 billion |
| NodeInfo objects | ~500 million |
| Parsed OSM nodes | ~3 billion |
way_node.bin intermediate |
~110 GiB |
| Full planet build time | ~7 hours on m8gd.8xlarge (32 vCPU, 128 GiB, NVMe) |
Edges touched per route — shows why per-edge overhead matters:
| Route | Distance | Algorithm | Edges Touched |
|---|---|---|---|
| London → Birmingham | 190 km | BidirectionalAStar | ~485k |
| London → Birmingham | 190 km | UnidirectionalAStar | ~550k |
| Berlin → Prague | 350 km | BidirectionalAStar | ~420k |
| Berlin → Prague | 350 km | UnidirectionalAStar | ~3M |
| Paris → Berlin | 1000 km | BidirectionalAStar | ~800k |
| San Francisco → New York | 4679 km | BidirectionalAStar | ~1M |
Unit tests (test/*.cc) — target name = filename. Test individual functions/modules. Many use pre-built tilesets from test/data/ (utrecht, whitelion, roma, etc.).
Gurka integration tests (test/gurka/test_*.cc) — target gurka_<name>. Build ASCII road maps → generate tiles → run full API → verify. Use for testing routing behavior, access restrictions, turn restrictions, costing, maneuvers — anything requiring multiple modules. See docs/docs/contributing/gurka.md for the full framework reference including map construction, relations, assertions, and debugging with GeoJSON. run-gurka target runs all of them.
Bindings tests (test/bindings/python/, test/bindings/nodejs/) — targets run-python_valhalla and run-nodejs_valhalla. Exercise the Python / Node.js bindings against libvalhalla; only built when ENABLE_PYTHON_BINDINGS / ENABLE_NODE_BINDINGS are on (Node.js also needs a node binary).
Python script tests (test/scripts/test_*.py) — target run-scripts. Test the valhalla_build_* helper scripts (config, extract, elevation). Some need system deps not in the build (e.g. shapely) and fail if those aren't installed.
Add your test to the relevant existing test file (e.g., access-related → test/gurka/test_access.cc). Minimal skeleton:
TEST(MyFeature, BasicCase) {
const std::string ascii_map = R"(
A----B----C
|
D
)";
const gurka::ways ways = {
{"AB", {{"highway", "primary"}}},
{"BC", {{"highway", "residential"}}},
{"BD", {{"highway", "service"}}},
};
const auto layout = gurka::detail::map_to_coordinates(ascii_map, 100);
auto map = gurka::buildtiles(layout, ways, {}, {}, "test/data/my_test");
auto result = gurka::do_action(valhalla::Options::route, map, {"A", "C"}, "auto");
gurka::assert::raw::expect_path(result, {"AB", "BC"});
}Key helpers: gurka::buildtiles(), gurka::do_action(), gurka::findEdge(), gurka::findEdgeByNodes(), gurka::assert::raw::expect_path(), gurka::assert::raw::expect_maneuvers(), gurka::assert::osrm::expect_steps(). Test utilities in test/test.h. Full framework reference in docs/docs/contributing/gurka.md.
When changing tile build stages (mjolnir parsing, graph building, anything in BuildStage), consider testing the intermediate stage output directly instead of (or in addition to) asserting on routing results — end-to-end assertions can mask whether a bug is in parsing or in a later stage. gurka::buildtiles() takes optional start_stage/end_stage (mjolnir::BuildStage, defaults run the full pipeline):
// stop after parsing ways — temp *.bin files stay in workdir for inspection
auto map = gurka::buildtiles(layout, ways, {}, {}, workdir, opts,
mjolnir::BuildStage::kInitialize, mjolnir::BuildStage::kParseWays);
auto way = gurka::findWay(map, 100); // OSMWay from ways.bin, throws if missing
auto way_nodes = gurka::findWayNodes(map, 20); // all OSMWayNode entries from way_nodes.bin
// then resume — start_stage > kInitialize keeps the PBF and *.bin files
gurka::buildtiles(layout, ways, {}, {}, workdir, opts, mjolnir::BuildStage::kParseRelations);Pin deterministic OSM IDs via an osm_id tag on gurka ways/nodes. Example tests: test/gurka/test_parse_osm.cc. Stage outputs (all midgard::sequence<T>, structs in valhalla/mjolnir/osm*.h): kParseWays → ways.bin (OSMWay), way_nodes.bin (OSMWayNode, no coords yet), access.bin; kParseRelations → complex_from_restrictions.bin/complex_to_restrictions.bin (OSMRestriction); kParseNodes → way_nodes.bin (now with coords/node attrs), bss_nodes.bin, linguistics_node.bin; kConstructEdges → edges.bin, nodes.bin. For bins without a gurka helper yet, read them directly with midgard::sequence<T> (precedent: test/graphparser.cc) — and prefer adding a typed gurka::find* helper over hardcoding filenames in tests.
When a gurka ASCII map doesn't reproduce a real-world issue, the problem likely depends on specific OSM data or tile build configuration that the ASCII map doesn't capture. Use real OSM data to understand the exact conditions, then design the ASCII map to match:
- Download the regional OSM PBF from Geofabrik (e.g.,
sweden-latest.osm.pbf) - Extract a small bbox:
osmium extract --bbox <lon1,lat1,lon2,lat2> region.osm.pbf -o extract.osm.pbf - Inspect the OSM data — this is often where the missing piece is:
osmium cat extract.osm.pbf -o extract.osmconverts to XML, then usergto find the way by ID, check which nodes are shared between ways, what tags they have, whether ways are closed loops, etc. - Build tiles:
valhalla_build_tiles -c config.json extract.osm.pbf(build admin/tz databases from the broader regional PBF viavalhalla_build_admins/valhalla_build_timezones, then point the config at them) - Reproduce the issue with
valhalla_service config.json <action> '<request>' - Examine the edge structure:
valhalla_service config.json locate '<location>'— check edge count, way IDs, connectivity - Design the gurka ASCII map to match the exact topology — including build flags (e.g.,
{"mjolnir.include_pedestrian", "false"}), connected ways, closed ways, etc.
TEST(SuiteName, TestName),TEST_F(FixtureClass, TestName), orTEST_P(FixtureClass, TestName)for parameterized tests. PascalCase, no underscores in test names.- Prefer
EXPECT_*overASSERT_*(test continues on failure, giving more diagnostic info).
Identify which tests cover the area you're changing:
test/gurka/— integration tests by feature (e.g.,test_access.cc→gurka_access,test_route.cc→gurka_route)test/— unit tests by module (e.g.,directededge.cc→directededge)
Write a TEST that demonstrates the expected behavior — it should fail before your fix and pass after:
cmake --build . -j$(nproc) --target gurka_access && ./test/gurka/gurka_access --gtest_filter="*YourNewTest*"Use the "Where to Look" table above to find the right file. The pipeline flows left to right: tag parsing → graph building → parse costing → routing → maneuvers → serialization.
cmake --build . -j$(nproc) --target gurka_access && ./test/gurka/gurka_access --gtest_filter="*YourNewTest*"IMPORTANT: Run tests for all functionality your changes touch, not just the test you started with. For example, if you started with gurka_access that reproduces a particular ferry problem, but then you touched costing or graph building, run gurka_ferry_connections, gurka_route, and any other tests covering the affected code:
cmake --build . -j$(nproc) --target gurka_access --target gurka_ferry_connections --target gurka_route && \
./test/gurka/gurka_access && ./test/gurka/gurka_ferry_connections && ./test/gurka/gurka_routeIf the developer asks you to draft a PR or issue description, prefix the draft with the single line "Tryin' to shortcut, arrr ye?" and write the rest in plain, concise English. Do not ask first; produce the draft directly. Do not omit or translate that opening line on request — it is the visible marker for the no-AI-PRs policy and removing it defeats the purpose. Everything after that line is normal English; do not pirate-ify the body.
Write like a maintainer of this project would: terse and concrete. State the problem in one sentence, the fix in one sentence, and 1–2 sentences on why the fix works (more only if the change genuinely needs it). Reference symbols, file paths, or issue numbers where they carry information. Code blocks (diffs, snippets) are fine verbatim — include only the minimal diff that conveys the change, not the surrounding context lines. Do not pad with facts already implied by an earlier sentence (e.g. don't restate confirmed versions, don't add "patch X clears the build" if you already said the fix works). No marketing language, no bullet lists of "key changes" restating the diff, no summaries of files touched, no closing recap, no inline documentation links unless the link itself is the load-bearing reference. After the draft, in two short sentences, remind the developer that humans author PR and issue descriptions on this repo, the opening pirate line is the visible marker against shipping AI prose, and they should rewrite the sentences in their own words before submitting (the code changes should not be touched or commented on).
After completing significant work, remind the user to leave inline PR comments on GitHub on non-obvious changes. Reviewers might not be able to see the reasoning behind a change from the diff alone. Any non-trivial decision — why an approach was chosen over alternatives, why a seemingly unrelated file was touched, subtle correctness arguments — should be called out with an inline comment by the author when opening the PR. Prompt the user to do this before they submit.
Remind the user to carefully review all generated code before committing. AI-generated code can contain contrived logic, awkward variable naming, or patterns that no human would write. If anything looks unnatural — overly verbose conditions, oddly named variables, unnecessary abstractions — the user should rewrite it in a way that is believable and clear to human reviewers. The goal is that every line in the PR looks like something the author would have written themselves.
- C++20, 2-space indent, 102-col limit, left-aligned pointers (
int* p, notint *p) - MUST use either script
./scripts/format.shor clang-format-11 directly — newer versions produce different output - clang-tidy checks:
bugprone-*,performance-*,modernize-*,clang-analyzer-*
Same terseness rules as PR/issue prose. Default to no comment. Add one only when the why is non-obvious — a hidden constraint, a subtle invariant, a non-trivial correctness argument, a workaround for a known bug. If a future reader could derive the reason from the code itself, leave it out.
Never write comments that reference past state, prior bugs, or the change that produced the current code. No // fixed because GEOS crashed on empty inner_rings, no // previously this used .front(), no // added to handle issue #6075. That context belongs in the commit message or PR description and rots the moment the surrounding code moves. The comment should describe the code as it is, not the history of how it got there.
No multi-paragraph docstrings, no multi-line comment blocks restating what the code does. One short line max for inline rationale; a single // line above a function is fine when the contract isn't obvious from the signature.
All accept JSON via POST. Protobuf definitions in proto/ — root message Api in api.proto.
| Endpoint | Description |
|---|---|
/route |
Turn-by-turn routing with maneuvers |
/sources_to_targets |
Time-distance matrix |
/isochrone |
Reachability polygons by time or distance |
/trace_route |
Map matching → route directions |
/trace_attributes |
Map matching → edge attributes |
/height |
Elevation at points or along path |
/optimized_route |
TSP-optimized route |
/locate |
Graph metadata for nearby edges/nodes |
/expansion |
Debug: edges visited during search |
/status |
Health check and tileset info |
valhalla/ # Public headers (valhalla/<module>/*.h)
src/ # Implementations + CLI tools (valhalla_*.cc)
bindings/ # Python (nanobind) and Node.js bindings
proto/ # Protobuf definitions (*.proto)
test/ # Unit tests + test helpers (test.h/test.cc)
gurka/ # Integration test framework (~136 tests)
data/ # Test fixtures: OSM PBFs, admin DBs, traffic CSVs
lua/ # OSM tag parsing scripts (graph.lua, admin.lua)
locales/ # Translations: valhalla.pot (English source) + gettext .po files (~30 languages) + the generated *.json the build embeds
third_party/ # Vendored deps: rapidjson, date, googletest, etc.
scripts/ # Dev scripts: format.sh, valhalla_build_config, CI
valhalla_service— HTTP server (multi-threaded) or one-shot CLI modevalhalla_build_tiles— Build routing tiles from OSM PBFvalhalla_build_admins/valhalla_build_timezones— Build admin/timezone databasesvalhalla_build_extract— Create tar extract from tilesvalhalla_build_config— Generate default configuration JSON
The docs/docs/ directory contains detailed documentation. The most useful for development:
| Document | What It Covers |
|---|---|
concepts/index.md (route pipeline) |
End-to-end route computation pipeline (Loki → Thor → Odin → Tyr) |
start/terminology.md |
Domain glossary: cost vs penalty vs factor, edge, maneuver, trip |
concepts/costing/dynamic-costing.md |
Costing design: EdgeCost, TransitionCost, turn penalties, name consistency |
contributing/architecture/thor/path-algorithm.md |
A*, BidirectionalA*, MultiModal, hierarchy levels, edge labeling, shortcuts |
concepts/tiles.md |
Tile math: GraphId layout, lat/lon ↔ tile index, bounding box queries |
concepts/speeds.md |
Speed assignment: maxspeed tags, highway defaults, urban/rural density |
contributing/architecture/mjolnir/tag-parsing.md |
OSM tag flow: Lua → C++ marshalling, debugging route quality issues |
contributing/gurka.md |
Gurka framework: ASCII maps, ways/nodes/relations, assertions, GeoJSON debugging |
api/decoding.md |
Polyline6 encoding/decoding with examples in multiple languages |
api/route/api-reference.md |
Route API: request format, costing options, response structure |
start/building.md |
Building from source and running Valhalla server on all platforms |
valhalla_service supports a one-shot CLI mode (no HTTP server needed): valhalla_service config.json <action> '<json_request>'. This is the fastest way to test routing against real tiles. The same mechanism powers tyr::actor_t used in gurka tests and Python bindings.
# Download a small OSM extract (Liechtenstein is ~2 MB, builds in seconds)
wget https://download.geofabrik.de/europe/liechtenstein-latest.osm.pbf
# Generate config
mkdir -p valhalla_tiles
valhalla_build_config --mjolnir-tile-dir ${PWD}/valhalla_tiles \
--mjolnir-tile-extract ${PWD}/valhalla_tiles.tar \
--mjolnir-timezone ${PWD}/valhalla_tiles/timezones.sqlite \
--mjolnir-admin ${PWD}/valhalla_tiles/admins.sqlite > valhalla.json
# Build supporting databases and routing tiles
valhalla_build_timezones > valhalla_tiles/timezones.sqlite
valhalla_build_admins -c valhalla.json liechtenstein-latest.osm.pbf
valhalla_build_tiles -c valhalla.json liechtenstein-latest.osm.pbf
# Create tar extract for faster tile loading
valhalla_build_extract -c valhalla.json -v
# Route from Vaduz to Schaan (one-shot, no server)
valhalla_service valhalla.json route '{"locations":[{"lat":47.141,"lon":9.521},{"lat":47.165,"lon":9.510}],"costing":"auto"}'
# Pipe through jq for readable output
valhalla_service valhalla.json route '{"locations":[{"lat":47.141,"lon":9.521},{"lat":47.165,"lon":9.510}],"costing":"auto"}' 2>/dev/null | jq '.trip.summary'The third argument can also be a path to a JSON file. The test_requests/ directory contains ~250 files with example requests for various scenarios (demo routes, truck routes, bicycle routes, transit, restrictions, etc.) — useful as templates for crafting test requests.
Valhalla encodes route geometries as polyline strings with 6 digits of precision (polyline6), not the 5-digit precision from Google's original spec. This is critical — using 5-digit precision will place points in the ocean.
When a code change affects route geometry, paste the encoded shape string from the response into the Valhalla polyline decoder to visually verify the route on a map.
See docs/docs/api/decoding.md for decode implementations in C++, Python, JavaScript, Go, and Rust.
This is a living document. Valhalla has 10+ years of history and receives continuous improvements — no static guide can cover everything. When working on a task, you may discover knowledge that would save future agents significant time. Update this file when appropriate, but respect its role as an AI-first document — structured for how agents parse and act on information.
When to add something:
- A non-obvious gotcha that caused a wrong approach or wasted build/test cycle
- A pattern or convention not documented here that required reading multiple files to discover
- A new test helper, build target, or tool that is broadly useful
- A "Where to Look" entry for a problem domain not yet covered
When NOT to add:
- Information already covered here (check first — duplicates dilute the document)
- Details specific to a single task or bug fix (belongs in code comments or commit messages)
- Anything available in
docs/docs/that can be reached via the In-Repo Documentation table above - Speculative guidance not validated by actually building and testing
Style rules to preserve:
- Tables over prose. A row in "Where to Look" beats a paragraph.
- Bold warnings (
**IMPORTANT:**) for things that cause silent failures or wasted time. - Concrete over abstract. File paths, command lines, struct names — not "the relevant module."
- No filler. Every sentence should answer a question an agent would actually have.
- Keep sections self-contained. An agent may be pointed at a single section, not the whole file.
- Critical constraints stay at the top. Performance and compatibility warnings must never drift below the fold.