Turn-by-turn routing that never phones home.

In-process A* over a user-supplied DuckDB road-network graph. No OSRM, no Valhalla, no GraphHopper sidecar, no Docker, no online calls by default. Drop a routable graph on disk, register it, and plan multi-waypoint routes with turn-by-turn directions — entirely inside the enclave.

In-process A*DuckDB road graphAir-gapped by default vehicle / foot / cycleGPX 1.1KML 2.2Turn-by-turn
No sidecar, no container

A routing engine that lives inside the app

Routing services usually mean another server to accredit. Here the solver runs in-process against the same DuckDB engine the rest of the platform uses — one POST, one route.

  • Bring your own graph — a nodes + edges DuckDB file, built offline from OpenStreetMap (osm2pgsql recipe in the docs) or supplied by your GIS team.
  • Minimum viable schemanodes(id, lat, lon) and edges(id, from_node, to_node) is enough; everything else is synthesized.
  • Multi-waypoint solves — origin → intermediates → destination solved as a sequence of A* legs, with per-leg turn-by-turn instructions preserved.
  • A* with an admissible heuristic — great-circle distance over max road speed, backed by the .NET PriorityQueue.
  • Fast at mission scale — city/region graphs (~100k nodes, ~250k edges) solve in under a second.
POST /api/routing/solve
{
  "origin": { "lat": 34.0522, "lon": -118.2437, "name": "Start" },
  "dest":   { "lat": 34.0901, "lon": -118.3614, "name": "End" },
  "intermediates": [
    { "lat": 34.0736, "lon": -118.2401, "name": "Stop 1" }
  ],
  "options": {
    "travelMode":    "vehicle",
    "avoidHighways": false,
    "useEsriOnline": false,
    "snapRadiusM":   500
  }
}
→ Route { distanceM, timeSec, geojson, legs[].instructions[] }
Graph handling

Register a graph, get a router

One POST /api/routing/graphs validates the schema, backfills what's missing, and builds the indexes the solver's hot path needs.

🧮

Auto-synthesized columns

Missing geometry, length_m, cost, and reverse_cost are backfilled at registration — geometries from lat/lon and node pairs, costs from length over speed, one-ways encoded as 1e18 reverse cost.

Auto indexes

B-tree indexes on edges.from_node (A* expansion hot path), edges.to_node (reverse traversal), and nodes.id (path reconstruction) are created for you.

🚗

Travel modes

vehicle uses edge max_speed or highway-type defaults (motorway 110, trunk 90, primary 80, … footway 5). foot caps at 5 km/h and ignores motorway/trunk; cycle caps at 25 km/h.

🧭

Route Planner UI

ToolsDrawer → NAVIGATE → Route Planner: add waypoints by typing or clicking the map, solve, and read turn-by-turn directions. Named waypoint lists persist via the waypoints API.

📤

GPX 1.1 + KML 2.2 export

POST /api/routing/export/gpx for Garmin and handheld GPS units, /export/kml for Google Earth — routes and waypoint lists both export.

🗺️

SpatiaLite VirtualRouting

A complementary offline navigation path: SpatiaLite VirtualRouting over cached OSM road networks with Dijkstra pathfinding, turn-by-turn instructions, and voice guidance announcements at decision points.

Beyond the route

Analyze the network, not just the path

The same DuckDB graph feeds an in-process analytics layer — and when you genuinely need country-scale online routing, an explicit toggle is there.

🕸️

Graph analysis on the road graph

  • Onager — 54 graph algorithms as DuckDB table functions: PageRank, Louvain communities, connected components, Dijkstra, betweenness centrality, MST, TSP approximation, and more.
  • DuckPGQ — declarative SQL/PGQ property-graph querying with MATCH and ANY SHORTEST.
  • Practical wins — run connected components to find disconnected road areas before a mission; density to measure network completeness.
  • One APIPOST /api/graph/run against dbId "routing", edges table, done.
🌐

Esri World Routing — an honest online toggle

  • Set options.useEsriOnline = true (or check "Use ArcGIS Online" in the Route Planner) to solve via Esri's World Routing service.
  • Requires internet and burns Esri credits per route — a confirmation toast fires on first use each session, so nobody spends credits by accident.
  • Returns the same {geojson, legs, summary} contract, so the map renderer and turn-by-turn work identically offline and online.
  • The intended use: country-scale solves beyond the offline router's city/region scope.

The <1 s figure applies at the documented city/region scale (~100k nodes / ~250k edges); country-scale offline solving is out of scope by design. DuckPGQ requires DuckDB 1.5.1 or 1.5.4 — with the currently bundled 1.5.3 the extension may not load; the loader degrades gracefully and Onager remains available. Turn restrictions and real-time traffic are not consulted by the offline solver.

Route inside the wire.

Build a routable graph once, register it, and every operator on the workstation plans, solves, and exports routes with zero external dependencies.