Velo — Routing Engine

Route calculation in pure C, from PBF to path

Velo is an OpenStreetMap routing engine written in C11 with zero external dependencies. Six algorithms from Dijkstra to ALT landmarks, four vehicle profiles (car, truck, bike, foot), CSR graph format for cache-friendly queries, and direct PBF parsing with no intermediate database. Country-scale routing in 36–86ms. Compiles to WASM for browser demos.

52 tests 6 algorithms Zero deps C11 WASM ~7K LoC AGPLv3 + Trucking Exception
Why

A routing engine you can embed anywhere

OSRM and Valhalla are excellent routing engines, but they're large C++ projects with dozens of dependencies, heavy preprocessing pipelines, and multi-GB memory footprints even for small regions. Want to run routing in the browser? On a truck's onboard computer? In a WASM sandbox? You need something smaller.

Velo trades some features for radical simplicity: one PBF file in, routes out. No PostgreSQL, no preprocessing database, no protobuf compiler. Load a PBF (or a pre-built .vlg binary index for 100× faster loading), build a CSR graph, and start routing. The same code runs native, embedded, or in the browser via WASM.

Six algorithms, one API
Dijkstra, bidirectional Dijkstra, A*, bidirectional A*, ALT with landmarks, and bucket-heap Dijkstra. Pick the algorithm that matches your latency budget. ALT with 32 landmarks routes Hungary in 36–86ms.
Four vehicle profiles
Car, truck, bike, and foot. Each profile filters edges by access restrictions extracted from OSM tags. Truck profile avoids hgv=no roads. Bike avoids motorways. Per-edge bitmask filtering — zero overhead at query time.
CSR graph format
Compressed Sparse Row: nodes are contiguous, edges are contiguous, one pointer dereference to scan neighbors. 16 bytes/node, 12 bytes/edge. Cache-line friendly. Hungary (2.7M nodes) fits in ~110MB with landmarks.
Binary index (.vlg)
Pre-built binary graph format loads 100× faster than PBF parsing. ~100ms vs ~8 seconds for Hungary. Memory-mapped on POSIX for instant loading. Build once, serve forever. One file, one binary.

Fleet deployment. A Velo binary + country .vlg index on an onboard computer gives trucks offline routing with full truck profile. Same binary compiles to WASM for the dispatch dashboard. FuelWise uses Velo for station reachability calculations along the route.

Algorithms

From brute-force to landmark-guided search

  vl_route(graph, from, to, options)
       │
       ▼
  ┌──────────────┐
  │ Grid Index   │  O(1) nearest node lookup
  │ 1000x1000    │  (lat/lon → node_id)
  └──────┬───────┘
         │
         ├─── VL_ALGORITHM_DIJKSTRA ─────────▶ Explores 100%
         ├─── VL_ALGORITHM_DIJKSTRA_BIDIR ──▶ Explores ~50%
         ├─── VL_ALGORITHM_ASTAR ────────────▶ Explores 30-50%
         ├─── VL_ALGORITHM_ASTAR_BIDIR ─────▶ Explores ~25%   (default)
         ├─── VL_ALGORITHM_ALT ──────────────▶ Explores 5-15%  (with landmarks)
         └─── VL_ALGORITHM_BUCKET_HEAP ─────▶ O(1) amortized ops
6
Algorithms
36ms
ALT (Hungary)
2.7M
Nodes (Hungary)
100ms
.vlg Load Time

Query time comparison (Hungary, 2.7M nodes)

Algorithm Query Time Nodes Explored Speedup vs Dijkstra
Dijkstra 500–1000ms 100%
Bidirectional Dijkstra 250–500ms ~50% ~2×
A* 200–400ms 30–50% 2–3×
Bidirectional A* 130–285ms ~25% 3–4×
ALT (32 landmarks) 36–86ms 5–15% 10–15×

ALT landmarks

A* with Landmarks and Triangle Inequality. At startup, Velo computes shortest-path distances from every node to/from 16–32 landmark nodes. At query time, these precomputed distances provide a tighter lower bound than Haversine, dramatically pruning the search space.

  • Dual-mode — separate distance and time landmarks for shortest and fastest queries
  • 5–15× speedup over bidirectional A* on country-scale routes
  • Preprocessing — 4–8 seconds for 32 landmarks on Hungary
  • Memory — ~2.6GB for 32 landmarks, 2.7M nodes (compressed)
Vehicle Profiles

Car, truck, bike, foot — per-edge access control

Profile Avoids Use Case
VL_PROFILE_CAR Pedestrian-only paths Personal vehicles, default
VL_PROFILE_TRUCK hgv=no roads, residential, service Commercial logistics
VL_PROFILE_BIKE Motorways, trunk roads Cycling routes
VL_PROFILE_FOOT Motorways, trunk, primary Walking navigation

Profiles are implemented as bitmask flags on each edge, extracted from OSM access tags during PBF parsing. Filtering adds zero overhead at query time — it's a single bitwise AND per edge relaxation.

Optimization modes

Mode Optimizes Edge Weight
VL_WEIGHT_DURATION (fastest) Minimize travel time Deciseconds (max ~1.8h/edge)
VL_WEIGHT_DISTANCE (shortest) Minimize distance Millimeters (max ~4295km/edge)
API

REST, C library, or WASM — same code

REST API (port 8082)

# Route between two points (fastest car route)
curl "http://localhost:8082/api/v1/route?from=47.497,19.040&to=46.253,20.148"

# Truck profile, shortest distance, with geometry
curl "http://localhost:8082/api/v1/route?\
  from=47.497,19.040&to=46.253,20.148&\
  profile=truck&mode=shortest&geometry=true"

# Response:
# {
#   "status": "ok",
#   "route": {
#     "distance": 187432.5,
#     "duration": 7234.2,
#     "profile": "truck",
#     "mode": "shortest",
#     "geometry": "encoded_polyline..."
#   },
#   "meta": { "nodes_explored": 12543, "search_time_ms": 34.5 }
# }

C API

// Load graph from binary index (100ms)
VLGraph *g = vl_load_binary("hungary.vlg");

// Build ALT landmarks (4-8 seconds, once at startup)
VLLandmarks *lm = vl_landmarks_create(g, 32);

// Route: Budapest → Szeged, truck, fastest
VLRouteOptions opts = {
    .algorithm = VL_ALGORITHM_ALT,
    .profile   = VL_PROFILE_TRUCK,
    .weight    = VL_WEIGHT_DURATION,
    .landmarks = lm
};

VLRoute route;
VLStatus status = vl_route(g, from, to, &opts, &route);

if (status == VL_STATUS_OK) {
    printf("%.0fm in %.0fs\n", route.distance, route.duration);
}
vl_route_free(&route);
vl_landmarks_free(lm);
vl_graph_free(g);

Graph data structures

Node (16 bytes)

struct VLNode {
    VLCoordFixed coord;  // lat/lon * 1e7
    uint32_t edge_start;  // edge array idx
    uint32_t edge_count;  // outgoing edges
    int64_t osm_id;
};

Edge (12 bytes)

struct VLEdge {
    uint32_t target;    // target node
    uint32_t distance;  // millimeters
    uint16_t duration;  // deciseconds
    uint16_t flags;     // road + access
};
Under the Hood

CSR, grid index, degree-2 contraction

Graph optimizations

  • Degree-2 contraction — eliminates chain nodes (only 2 edges), reduces graph 40–60%
  • Grid spatial index — 1000×1000 cells for O(1) average nearest-node lookup
  • Reverse graph — pre-built reverse edge index for O(degree) backward search
  • Hilbert ordering — cache-friendly geographic node ordering
  • Fixed-point coordinates — 32-bit integers (lat × 107) instead of 64-bit doubles

Memory budget (country → continental)

Component Hungary (2.7M) US (~175M) EU (~350M)
Nodes 43MB 4.2GB 8.4GB
Edges 36MB 4.0GB 8.0GB
Landmarks (32) 2.6GB 8.4GB 16.8GB
Total ~3GB ~20GB ~37GB

Honest assessment. Velo is not OSRM. It doesn't have Contraction Hierarchies (sub-millisecond queries), turn restrictions, or traffic-aware routing. For country-scale truck routing, 36–86ms with ALT is sufficient. For consumer navigation competing with Google Maps, you'd want CH preprocessing (planned Phase 2) or a commercial engine behind the Velo API.

~7K
Lines of C
52
Tests
4
Profiles
0
Ext Dependencies
Quick Start

Three ways to try Velo

1. Browser (zero install)

The API documentation page includes a live WASM demo with embedded Monaco data. Calculate routes between Monaco locations in the browser.

2. Build from source

# Clone and build
git clone https://github.com/ottofleet/otto.git
cd otto && make velo

# Run all 52 tests
make test-velo

3. Route server

# Build and start server
make velo-api
./velo/api/velo-route-server data/hungary-latest.osm.pbf

# Or use pre-built index (100x faster loading)
./velo/api/velo-route-server --load-index data/hungary.vlg

# Calculate a route
curl "http://localhost:8082/api/v1/route?\
  from=47.497,19.040&to=46.253,20.148&profile=truck"
FAQ

Common questions

Why not use OSRM or Valhalla?

Both are excellent but large: OSRM is ~100K LoC of C++ with Boost, Lua, and TBB. Velo is ~7K lines of C with zero dependencies. It compiles to WASM, runs on an onboard computer, and loads a country graph in 100ms from a .vlg file. The tradeoff: OSRM has CH (sub-ms queries), turn-by-turn navigation, and traffic. Velo has ALT (36–86ms), four profiles, and radical simplicity.

How does FuelWise use Velo?

FuelWise calculates fuel-optimal refueling stops along a route. It uses Velo to compute distances between fuel stations along the path, then Ralph (LP solver) to optimize which stations to stop at and how much fuel to load. Velo provides the distance inputs; Ralph provides the optimization.

Can Velo handle continental-scale routing?

Not yet at consumer latency. The US graph (~175M nodes) needs ~20GB RAM with landmarks. The EU graph (~350M nodes) needs ~37GB. A 64-core server with 128GB RAM can serve continental routes in <2 seconds. For sub-millisecond queries, Contraction Hierarchies are planned for Phase 2.

What about truck-specific restrictions?

Velo's truck profile currently avoids hgv=no roads. The roadmap includes height, weight, length, and axle-count constraints from OSM tags, plus hazmat routing. These are stored as sparse restrictions (affecting ~1–5% of edges) to avoid memory bloat.

What license?

AGPLv3 with a Trucking Exception. Same terms as all OTTO modules.