Skip to content

Routing

Yoro Maps provides offline routing on an OSM road graph using the A* algorithm.

How routing works

  1. Nearest node lookup — Given start/end GPS coordinates, the router finds the closest graph node using an expanding bounding-box search.
  2. A* search — The algorithm explores the graph using a priority queue, with the haversine straight-line distance as the heuristic.
  3. Path reconstruction — Once the destination is reached, the path is traced back through the came_from map.
  4. Result building — The router builds a GeoJSON LineString geometry and turn-by-turn navigation steps.

Route between coordinates

import yoromaps

conn = yoromaps.open_db("mali.yoromaps")
result = yoromaps.route(
    conn,
    start_lat=12.6392,
    start_lon=-8.0029,
    end_lat=14.4974,
    end_lon=-4.0077,
)

if result.found:
    print(f"Distance: {result.distance_km} km")
    print(f"Duration: {result.duration_min} min")
    print(f"Nodes:    {len(result.nodes)}")
else:
    print("No route found")

Route between Yoro codes

Yoro codes are decoded to GPS coordinates using the yoro library, then routed:

import yoromaps

conn = yoromaps.open_db("mali.yoromaps")
legs = yoromaps.route_from_codes(conn, ["ML-ABC", "ML-XYZ"])

for i, leg in enumerate(legs):
    print(f"Leg {i + 1}: {leg.distance_km} km, {leg.duration_min} min, found={leg.found}")

Multi-leg routes

Pass more than 2 codes to get multiple legs:

legs = yoromaps.route_from_codes(conn, ["ML-AAA", "ML-BBB", "ML-CCC", "ML-DDD"])
# Returns 3 legs: AAA->BBB, BBB->CCC, CCC->DDD

total_km = sum(leg.distance_km for leg in legs)
total_min = sum(leg.duration_min for leg in legs)
print(f"Total: {total_km} km, {total_min} min")

CLI routing

# Two-point route
yoromaps route mali.yoromaps ML-ABC ML-XYZ

# Multi-leg route
yoromaps route mali.yoromaps ML-AAA ML-BBB ML-CCC

Output is JSON:

{
  "total_distance_km": 642.3,
  "total_duration_min": 578.1,
  "legs": [
    {
      "distance_km": 642.3,
      "duration_min": 578.1,
      "found": true,
      "steps": [
        {"instruction": "Continue on Route Nationale 6", "distance_m": 12340, "name": "Route Nationale 6"},
        {"instruction": "Arrive at destination via RN1", "distance_m": 8500, "name": "RN1"}
      ]
    }
  ]
}

RouteResult object

The route() function returns a RouteResult dataclass:

Field Type Description
distance_km float Total distance in kilometers
duration_min float Estimated travel time in minutes
nodes list[int] List of graph node IDs along the route
geometry dict GeoJSON LineString with the route coordinates
steps list[dict] Turn-by-turn navigation instructions
found bool Whether a route was found (default True)

GeoJSON output

The geometry field is a standard GeoJSON LineString that can be displayed on any map:

import json

result = yoromaps.route(conn, start_lat=12.6, start_lon=-8.0, end_lat=14.5, end_lon=-4.0)

# Save as GeoJSON Feature
feature = {
    "type": "Feature",
    "geometry": result.geometry,
    "properties": {
        "distance_km": result.distance_km,
        "duration_min": result.duration_min,
    },
}

with open("route.geojson", "w") as f:
    json.dump(feature, f)

This GeoJSON can be loaded directly in QGIS, geojson.io, or rendered on a Leaflet map.

The route() convenience function reloads the graph on every call. For applications that compute many routes (e.g. a Django API), load the graph once and reuse it:

import yoromaps

conn = yoromaps.open_db("mali.yoromaps")
graph = yoromaps.Graph.from_db(conn)  # load once (~20s for Mali)

# Route many times — fast
r1 = graph.route(12.639, -8.003, 14.489, -4.197)  # Bamako → Mopti
r2 = graph.route(12.639, -8.003, 11.317, -5.667)  # Bamako → Sikasso
r3 = graph.route(12.63, -8.00, 12.65, -7.98)      # short urban route

In Django, load the graph once at startup:

# apps.py or a module-level singleton
import yoromaps

_graph = None

def get_graph():
    global _graph
    if _graph is None:
        conn = yoromaps.open_db("/data/mali.yoromaps")
        _graph = yoromaps.Graph.from_db(conn)
    return _graph

# views.py
def route_view(request):
    graph = get_graph()
    r = graph.route(lat1, lon1, lat2, lon2)
    ...

Performance (tested on real data)

The graph is loaded entirely into memory for fast routing. Tested results:

Mali (6.4M nodes, 6.7M edges)

Route Distance Duration Compute time
Bamako → Mopti 551.8 km 11h21 6.3s
Bamako → Sikasso 338.1 km 7h18 2.4s
Bamako → Segou 325.9 km 5h56 2.6s
Bamako → Kayes 501.0 km 10h00 3.4s
Bamako → Tombouctou 812.2 km 20h38 6.6s
Court (Bamako centre) 3.8 km 6 min 0.5s

Graph loading: ~20s. Memory: ~200 MB.

Togo (2.2M nodes, 2.3M edges)

Route Distance Duration Compute time
Lome → Kara 411.8 km 4h40 3.0s
Court (Lome centre) 7.4 km 10 min 0.2s

Graph loading: ~7s. Memory: ~100 MB.

Urban routing

Most real-world usage will be intra-city routing (e.g. within Bamako, Ouagadougou, Niamey). These routes are short (<20 km) and compute in under 1 second. Combined with Yoro precision p=19 (~1.35m), this gives door-to-door directions.