MPEE · Rust routing engine

Offline route calculations & optimization

One Rust engine that replaces the OSRM + VROOM stack — routing and vehicle-routing optimization in a single process. Download one area once, then route, optimize, and geocode it fully offline.

MPEE builds a 50,000 × 50,000 distance matrix in ~500 MB — the same matrix is ~10 GB materialised, the way OSRM + VROOM do it (≈20× more memory). MPEE streams instead of storing, finishing in 94 s where OSRM runs out of RAM, on CPU + GPU (Metal on Mac).

PyPI version of mpee Supported Python versions MIT license Source on GitHub
≤ 500 MB
our 50k×50k matrix — vs ~10 GB materialised (OSRM + VROOM), ≈20× less
94 s
to build that 50k² matrix — OSRM runs out of memory
CPU + GPU
GPU-accelerated VRP search (Metal on Mac)
50,000
-customer VRP on a laptop, fully offline
MPEE live demo: address search, street-crossing lookup, point-to-point routing and multi-vehicle optimization over San Francisco, all running in the browser ▶ Live demo — address search, street crossings, routing & multi-vehicle optimization, all in your browser (WebAssembly + WebGPU)

What it is

MPEE is built from two Rust crates — dijeng (contraction-hierarchy routing) and brooom (the VRP solver). Together they solve a 50,000-customer vehicle-routing problem on a laptop without ever materialising a full distance matrix, and in head-to-head tests on a Mac they produced shorter routes than VROOM at equal runtime, using less memory. The engine binary is under ~50 MB; the map cache scales with the area you download.

🧭

Point-to-point routes

Driving distance + time between two coordinates, with optional geometry.

🚚

Multi-vehicle VRP

Best routes for N vehicles over your own stops — capacities, skills, time windows, mixed fleets.

🔎

Offline geocoding

Street ⇄ coordinate and street-crossing lookup, reusing the same cache. No separate index.

🗺️

Bring your own area

Any OpenStreetMap extract sized to where you operate. One downloaded area, one cache.

Scope: MPEE covers a single downloaded area — it is not a route-anywhere-on-Earth offline map. There is no global tiling, by design: within your area, one cache is simpler and faster.

How it compares (measured, Apple M3 Pro)

A 50,000-customer fleet implies a 50,000 × 50,000 distance matrix — ~10 GB if you store it. The classic OSRM + VROOM split builds and ships that matrix between two processes; MPEE streams it in one process and never materialises it — which is where the speed and memory wins come from.

Routing — N×N duration+distance matrix · dijeng vs OSRM, Greater London CH (n = 1.16 M)

MatrixMPEE — timeMPEE — peak RAMOSRM
10k × 10k4.3 sstreamedimpractical — no chunked many-to-many
50k × 50k94 s≤ 500 MBOOM — the matrix alone is ~10 GB

Honest caveat: OSRM is ~3× faster on a single point-to-point query (≈30 µs vs ≈88 µs). MPEE wins decisively the moment you need a fleet-sized matrix — the case VRP actually requires.

Optimisation — VRP solver · brooom vs PyVRP / VROOM / OR-Tools, Solomon-style

ScaleResult
N = 1,000beats the next-best solver (PyVRP) on 17 / 20 seeds, p ≈ 10⁻⁷
N = 50,000the only tested solver that converges on a laptop
Inner loopthe entire local search (2-opt, relocate, swap-star, Or-opt, ILS-kick, regret-3) as one GPU megakernel — Metal on Mac, Vulkan/DX12 elsewhere; sub-ms per iteration

End-to-end on this machine: 2,000 jobs / 50 vehicles in ~2 min (matrix 0.32 s); 5,000 / 100 in ~9 min (matrix 4.10 s), both ≥99 % assigned. Full numbers in the dijeng and brooom READMEs.

Install

pip install mpee

Prebuilt wheel for macOS (arm64); Linux/Windows install from the source distribution (needs a Rust toolchain) until multi-platform wheels are published. Python 3.8+. No Rust toolchain needed for the macOS wheel.

Quick start (CLI)

1. Install & get a map once

# MPEE — offline routing, VRP & street geocoding for one downloaded area.
# Docs: https://punnerud.github.io/mpee/   Source: https://github.com/punnerud/mpee
pip install mpee

# Download an OpenStreetMap extract (Geofabrik) …
mpee download europe/great-britain/england/greater-london

# … and preprocess it into a routable cache (.pp + .ch + .names).
mpee build data/greater-london-latest.osm.pbf            # car (default)
# mpee build data/greater-london-latest.osm.pbf bicycle  # or: car | bicycle | foot

After this you are fully offline. The cache is reusable; a re-run reuses it instantly (--force rebuilds, --quiet hushes progress).

2. Route from A to B

mpee route 51.5080,-0.1281 51.5138,-0.0984 --cache data/greater-london-latest.osm.pbf
#   distance: 2.38 km
#   duration: 4.4 min

3. Optimize a delivery run over many stops

# stops.txt: one "lat,lon" per line (or a JSON [[lat,lon], …])
mpee optimize --stops stops.txt --vehicles 5 --capacity 20 \
    --cache data/greater-london-latest.osm.pbf
#   stops: 50  vehicles used: 3/5  unassigned: 0
#   total: 60.0 km, 115 min  (solved in 4.6s)

4. Geocode within the area (offline)

mpee reverse  51.5080,-0.1281 --cache data/greater-london-latest.osm.pbf  # → Trafalgar Square
mpee geocode  "Baker Street"  --cache data/greater-london-latest.osm.pbf  # → 51.522072,-0.157497
mpee crossing "Oxford Street" "Regent Street" --cache ...                 # → Oxford Circus (LAT,LON)

Use it from Python

Coordinate order. The simple helpers take (lat, lon). The VROOM-style solve(problem) accepts a coordinate as {"lat": …, "lon": …} (recommended), [lon, lat], or {"coord": [lon, lat]}.

import mpee

# Open a prebuilt cache (built once via `mpee build`).
r = mpee.Router("data/greater-london-latest.osm.pbf.pp",
                "data/greater-london-latest.osm.pbf.ch")

# Distance + time between two points.
leg = r.route(51.5080, -0.1281, 51.5138, -0.0984)
print(leg["distance_km"], "km,", leg["duration_min"], "min")

# Optimize 50 deliveries across 5 vehicles.
stops = [(51.51, -0.12), (51.49, -0.10)]          # your (lat, lon) list
plan = r.optimize(stops, vehicles=5, capacity=20, time_limit_s=5.0)

# Geocoding (needs the .names sidecar built by `mpee build`).
r.reverse(51.5080, -0.1281)                        # → "Trafalgar Square"
r.geocode("Baker Street")                          # → {"name", "lat", "lon"}
r.intersection("Oxford Street", "Regent Street")   # → [{"lat", "lon"}, …]

# Other helpers: r.snap(lat, lon), r.table(stops), r.bbox(),
#                r.has_names(), r.has_routing()

Build a cache straight from Python with mpee.Router.build("area.osm.pbf", profile="car") — it reuses an existing cache and returns instantly unless you pass force=True.

Geocoding without a separate index

Street names live on the OpenStreetMap road, so MPEE attaches them to the road nodes during the build and writes a small, deletable .names sidecar. Reverse reuses the routing snap grid; forward scans the area's distinct street names; crossing is the set intersection of two streets' node lists. Street-level (street + coordinate), not house numbers.

Lightweight: geocoding needs no .ch

Open with the .pp alone to skip loading the largest cache file:

r = mpee.Router("area.osm.pbf.pp")     # geocoding-only (no ch_path)
r.geocode("Baker Street")              # works
r.has_routing()                        # → False  (route/optimize need a .ch)

# CLI equivalent:
mpee geocode "Baker Street" --pp area.osm.pbf.pp

Multi-city caches: disambiguate with --near

Street names are unique only within a downloaded area. On a country cache the same name exists in several towns, so add a reference point:

# nearest "Munkegata" to a point (Trondheim, not an arbitrary first hit)
mpee geocode "Munkegata" --near 63.43,10.40 --cache norway.osm.pbf

# crossings near a point, within a radius
mpee crossing "Prinsens gate" "Kongens gate" --near 63.43,10.40 --radius-km 5 --cache norway.osm.pbf

# Python: r.geocode("Munkegata", near=(63.43, 10.40))
#         r.intersection("Prinsens gate", "Kongens gate", near=(63.43, 10.40), radius_km=5)

Real fleets: per-vehicle & per-stop constraints

For mixed fleets and constrained jobs, use Router.solve(problem) with a VROOM-style JSON problem. It exposes the engine's full model:

Per vehiclePer stop
capacity (multi-dimensional)delivery / pickup (package sizes / weights)
skills — which jobs it may serveskills — required vehicle capability
speed_factor — e.g. a motorcycle at 1.6time_windows — allowed arrival times
time_window — the driver's shiftservice — time spent at the stop
max_travel_time / max_distancepriority — which jobs to keep when demand > capacity
distinct start / end locations

solve() serves every job it feasibly can and returns the rest in unassigned (over capacity, outside all time windows, or no road to them) — it never invents an impossible route. Model a multi-day work week by giving each driver one vehicle per day bound to that day's shift window.

How it works

pip install mpee ships a compiled Rust extension plus a thin Python CLI. All routing and optimization run in-process; the map cache is memory-mapped, so opening it is near-instant and peak RAM stays low. Nothing is sent to a server.

Cache files (built next to your .osm.pbf)

FilePurposeNeeded by
.ppPreprocessed graph + coordinateseverything (snap, geocode, route)
.chContraction-hierarchy shortcutsroute / optimize / solve / table
.namesStreet names + per-street nodesreverse / geocode / crossing (deletable)

Cache size scales with map area: a city ≈ tens of MB, a whole country ≈ gigabytes. Download the area you operate in — not the whole world.

Quick reference (for agents & LLMs)

A compact, copy-friendly summary of the whole API surface.

Install & build

pip install mpee                              # Python 3.8+, macOS wheel / sdist elsewhere
mpee download <geofabrik/slug>                # fetch an OSM .osm.pbf extract
mpee build <area.osm.pbf> [car|bicycle|foot]  # → area.osm.pbf.pp + .ch + .names
#   flags: --force (rebuild), --quiet (hush), --keep-csr (keep parse cache)

CLI verbs (the `mpee` command; --cache PREFIX = the .osm.pbf path)

mpee route   LAT,LON LAT,LON      --cache <pbf>   # distance + time
mpee optimize --stops F --vehicles N --capacity C --cache <pbf>   # VRP
mpee reverse LAT,LON              --cache <pbf>   # coord → nearest street
mpee geocode "Street"            --cache <pbf>   # street → LAT,LON  (+ --near LAT,LON)
mpee crossing "A" "B"            --cache <pbf>   # where two streets cross (+ --near, --radius-km)
#   geocoding verbs accept --pp <file> alone (no .ch needed)
#   add --json to route/optimize/reverse/geocode/crossing for scriptable output

Python API

import mpee
r = mpee.Router(pp_path, ch_path=None)   # ch_path=None → geocoding-only (skips .ch)
mpee.Router.build(pbf, profile="car", progress=True, force=False) -> dict

# routing (need a .ch):
r.route(from_lat, from_lon, to_lat, to_lon, geometry=False) -> dict
r.optimize(stops, vehicles=1, capacity=1_000_000, depot=None, time_limit_s=5.0) -> dict
r.solve(problem_json, time_limit_s=5.0) -> dict     # VROOM-style JSON
r.table(points) -> dict                              # N×N durations + distances
r.snap(lat, lon) -> (lat, lon)
r.bbox() -> dict
# geocoding (need a .names sidecar):
r.reverse(lat, lon) -> str | None
r.geocode(query, near=None) -> {"name","lat","lon"} | None
r.intersection(a, b, near=None, radius_km=None) -> [{"lat","lon"}, …]
r.has_names() -> bool        # geocoding available
r.has_routing() -> bool      # routing available (a .ch was loaded)

Facts