Client-side routing library for OpenMapTiles vector tiles. Computes optimal routes and isolines for car, pedestrian, and bicycle travel entirely in the browser — no external routing backend required.
This repository is provider-agnostic for OpenMapTiles-compatible vector tiles, so the same tiles used for your basemap can be used for routing (reduces network and operational overhead).
- Overview
- Key features
- Available routing engines
- Engine selector
- Quick start
- MapLibre integration
- API reference
- Architecture
- Transport modes & road classes
- Development
- Caveats
- License
omt-router builds a routing graph from OpenMapTiles-formatted vector tiles in Web Workers, then runs routing algorithms in the browser. It is designed for: high performance, deterministic tile stitching across tile boundaries, and flexible engine selection to trade runtime vs reliability.
The library supports route finding, isoline (reachability) polygons, worker pools, tile caching, and a lightweight MapLibre control for quick integration into web maps.
- Zero backend — client-side graph construction and routing; no third-party routing API required.
- Routing and isolines (isoPHAST) for
car,pedestrian, andbicyclemodes. - Multi-engine routing:
bidirectional-astar,adaptive-barrier,delta-stepping,ultra-dijkstra. - Automatic runtime engine selector (benchmark-trained) with conservative fallbacks.
- Endpoint snapping with configurable quality guard (
maxAcceptableSnapDistanceM). - Seamless tile stitching using Liang–Barsky clipping to ensure bit-identical boundary nodes across tiles.
- Worker pool (
PowerPool) + tile parse cache (PowerCache) for parallel parsing and reuse.
For details on the training and benchmark pipeline, see benchmark/README.md.
The library exposes several engines. Use engineId to force a specific engine, or auto to use the runtime selector.
| Engine ID | Algorithm | Parallel ready | Best fit |
|---|---|---|---|
bidirectional-astar |
Bidirectional A* with geographic heuristic | Stable performance on sparse or long routes | |
adaptive-barrier |
Adaptive Barrier SSSP | ✓ | Dense, medium/large graphs; benefits from parallel runtime |
delta-stepping |
Delta-Stepping SSSP | ✓ | Large frontiers and bursty relax phases |
ultra-dijkstra |
Optimized Dijkstra (4-ary heap) | Reliable baseline and fallback |
Notes:
queryRoute()androute()acceptoptions.engineIdto force a specific engine.- When an engine returns invalid/no-path, the router can fall back to
bidirectional-astarfor correctness.
An ML-based selector chooses the engine per request to minimize runtime while keeping correctness risk low. The compact runtime artifact is src/tuning/model.js and inference lives in src/tuning/tuning.js.
- Validation: selects the optimal engine in ~91% of validation cases; selects a “good-enough” engine ~92% of the time (average runtime within ~6–7% of optimal).
- Trained on 16,646 samples; supports
sabOn(SharedArrayBuffer/worker-enabled) andsabOffprofiles. - Conservative defaults:
ultra-dijkstrais used as a low-risk fallback; thresholds can be tuned for canary rollouts.
At runtime the selector evaluates features such as beeline distance, E (edge count), N (node count), density/branching indicators, and available runtime capabilities (SharedArrayBuffer, Worker availability, cross-origin isolation).
Install:
npm install omt-routerBasic route() example:
import { route } from 'omt-router';
const metadata = await fetch('https://tiles.openfreemap.org/planet').then((r) => r.json());
const urlTemplate = metadata.tiles[0];
const result = await route(
[-3.7038, 40.4168], // origin [lng, lat]
[-3.6937, 40.4101], // destination
'car', // 'car' | 'pedestrian' | 'bicycle'
urlTemplate,
{ costField: 'travelTime' } // 'distance' | 'travelTime' | 'optimal'
);
console.log(result.found); // true
console.log(result.coordinates); // [[lng, lat], ...]
console.log(result.cost); // total route cost for the selected costFieldMapTiler or other providers also work:
const urlTemplate = 'https://api.maptiler.com/tiles/v3-openmaptiles/{z}/{x}/{y}.pbf?key=YOUR_KEY';Returned RouteResult highlights:
| Field | Type | Description |
|---|---|---|
found |
boolean |
Whether a path was found |
path |
number[] |
Sequence of internal node IDs |
coordinates |
[number, number][] |
[lng, lat] pairs ready for GeoJSON |
cost |
number |
Total route cost (distance in metres or travelTime in seconds) |
costField |
string |
Cost field used (distance / travelTime / optimal) |
partialGraph |
boolean |
true when computed against a graph with missing tiles |
omt-router provides a MapLibreRoutingControl for easy integration with MapLibre GL JS.
Example usage:
import {
MapLibreRoutingControl,
route,
getEngineWorkerStatus,
onEngineWorkerStatusChange,
cancelRunningEngine,
} from 'omt-router';
const control = new MapLibreRoutingControl({
routeFunction: route,
getEngineWorkerStatus,
onEngineWorkerStatusChange,
cancelRunningEngine,
tileJsonUrl: 'https://tiles.openfreemap.org/planet',
maplibre: maplibregl,
});
map.addControl(control, 'top-left');Constructor options (selected):
| Option | Type | Default | Description |
|---|---|---|---|
maplibre |
object |
— | Required MapLibre GL object (maplibregl). |
tileJsonUrl |
string |
— | Metadata endpoint returning { tiles: [urlTemplate] }. |
urlTemplate |
string |
— | Optional {z}/{x}/{y}.pbf template; if present, no metadata fetch is required. |
routeFunction |
function |
route |
Custom route implementation returning the routing result. |
getEngineWorkerStatus |
function |
— | Optional callback that returns engine worker status. |
onEngineWorkerStatusChange |
function |
— | Subscription callback for engine status changes. |
cancelRunningEngine |
function |
— | Cancel callback used when a route request times out or control is removed. |
defaultMode |
string |
car |
Initial transport mode: car, pedestrian, or bicycle. |
defaultCostField |
string |
distance |
Optimization target: distance, travelTime, or optimal. |
theme |
string |
light |
UI theme: auto, light, or dark. |
panelClassName |
string |
`` | Additional CSS class(es) for the control panel. |
routeTimeoutMs |
number |
20000 |
Route request timeout (ms). |
routeOptions |
object |
{ maxAutoRadius: 8, maxAcceptableSnapDistanceM: 60 } |
Forwarded to route engine. |
showGraph |
boolean |
false |
Whether to render the internal graph overlay. |
features |
string |
both |
Which features to show: routing, isolines, or both. |
isolineMaxCost |
number |
1000 / 900 |
Default isoline max cost (meters for distance, seconds for travelTime). |
All unspecified text in locale_override is merged from the selected built-in locale. See the examples in the repository for localization overrides.
setOrigin(lngLat)— set origin and trigger route update.setDest(lngLat)— set destination and trigger route update.setUrlTemplate(urlTemplate)— update tile URL template and refresh route.setTileJsonUrl(url)— update tile metadata URL, fetch template, refresh route.dispose()/shutdown()— free worker pools and caches.
- Integrated isoline (isoPHAST) UI computes reachability polygons for a selected point.
features === 'both'renders tabs for routing and isolines; switching clears the alternate feature's layers/sources.- Isolines are output to
isolineSourceIdas a GeoJSONFeatureCollection; each feature includesproperties.colorused for styling. - Default isoline behaviour: direction
from,isolineMaxCostdefaults to1000m fordistance,900s fortravelTime.
This section summarizes the most important public functions and options. For full details, consult the source in src/.
High-level convenience function. Fetches tiles, builds the graph, and returns the route result.
Key options (selected):
zoom(number) — default14.schema('zxy'|'tms') — default'zxy'.maxAutoRadius(number) — default8.engineId(string) —'auto'(default) or explicit engine id.costField(string) —'distance'(default),'travelTime', or'optimal'.penalties(object) —{ intersectionPenaltySec, turnPenaltySec, turnAngleThresholdDeg }.maxAcceptableSnapDistanceM(number) — default60.includeGraph(boolean) — include prepared graph for debugging.
Returned result includes runtime metadata: engine, optional fallback, startSnapDistanceM, endSnapDistanceM, partialGraph, hasMissingTiles, missingTileErrors, and reason on failure.
Formats a {z}/{x}/{y} tile URL and supports tileProxyTemplate and tileUrlTransform hooks.
Example:
const tileUrl = buildTileURL(
'https://example.com/{z}/{x}/{y}.pbf',
{ z: 14, x: 4827, y: 6372 },
{
tileProxyTemplate: '/api/tile?url={url}',
}
);Build a prepared routing graph from an explicit tile list without running route(). Useful for preloads and reusing graphs across requests.
Example:
const tiles = [
{ z: 14, x: 4827, y: 6372 },
{ z: 14, x: 4828, y: 6372 },
];
const graph = await buildGraphForTiles(tiles, 'car', { urlTemplate });Run many route() requests in parallel while sharing tile cache and worker pools.
Run routing on a pre-built graph (when you manage tile loading yourself). Supports costField, penalties, snapDistancesM, maxAcceptableSnapDistanceM, and engineId hints.
Convert merged graph into compact typed arrays and CSR adjacency for engine execution; costs are scaled for fast inner-loop arithmetic.
Run the routing algorithm on a buildCH-prepared graph. Supports engineId: 'auto' and explicit engine IDs, useCache, allowFallback, and parallel/serial policies.
Return the nearest graph node id within maxDistM (default 500 m).
High-level flow:
route()
│
├─ tilesManager → corridor tiles (+ adaptive radius retries)
│
├─ graphBuilder
│ ├─ PowerPool workers → fetch + parse transportation layer
│ ├─ PowerCache → per-tile parse cache
│ └─ mergeSegments → graph nodes/edges with distance + travelTime
│
└─ chRouter
├─ buildCH → typed arrays + forward/reverse CSR
├─ nearestNode → endpoint snapping via KDBush
└─ queryRoute → auto engine select, worker run, validate, fallback
Key implementation notes:
- Tile corridor enumeration uses Bresenham rasterisation to select slippy-map tiles around the origin→destination corridor and supports adaptive radius retry loops for robustness.
- Tile parsing is parallelised using PowerPool workers and cached in PowerCache to avoid repeated decoding.
- Road segments are clipped to tile boundaries using Liang–Barsky clipping; adjacent tiles produce bit-identical boundary coordinates, avoiding proximity snapping.
- Graph merging deduplicates nodes with a coordinate hash (rounded to 6 decimal places) and computes edge costs and travel times from per-class defaults.
Road filtering follows the OpenMapTiles transportation schema.
| Mode | Allowed classes | Excluded |
|---|---|---|
car |
motorway(_link), trunk(_link), primary(_link), secondary(_link), tertiary(_link), minor, service, track | pedestrian/footway/cycleway/steps/bridleway/corridor subclasses |
pedestrian |
path, minor, service, track (+ pedestrian/footway/steps/path/corridor/platform subclasses) | motorways and non-foot-access roads |
bicycle |
path, minor, service, tertiary, secondary, track (+ cycleway/path subclasses) | motorway, motorway_link, non-bicycle-access roads |
Default per-class speeds are used to compute travelTime (km/h). These defaults produce conservative, predictable travel-time estimates.
Common commands:
npm install # install dependencies
npm run dev # Vite dev server (serves example/)
npm run build # build library to dist/
npm run test # run Vitest test suite
npm run lint # ESLint
npm run format # PrettierServe the example/ demo locally with npm run dev and open http://localhost:5173/example/.
Training note: npm run train runs the selector training script benchmark/train_engine_selector_ml.py using .venv/bin/python. Ensure a Python virtualenv is available or update the script to point to your interpreter.
- Route quality depends on OpenStreetMap data coverage and tagging. If routing inaccuracies appear, consider improving source OSM data.
- Endpoints must snap to routable graph edges; snaps beyond
maxAcceptableSnapDistanceMwill fail with a clear reason (no_node,poor_snap). - Tile servers must allow CORS for uncached cross-origin tile requests — otherwise use
tileProxyTemplateto proxy tiles through a same-origin endpoint.
If you see reason: 'tile_cors' with code: 'MissingAllowOriginHeader' the browser blocked a cross-origin uncached tile request. Fix by adding CORS headers or configuring tileProxyTemplate.
- Live demo
- OpenMapTiles schema: https://openmaptiles.org/schema/#transportation
- OpenFreeMap metadata used in the example: https://tiles.openfreemap.org/planet
- Performance helpers: https://abelvm.github.io/performance-helpers
- Benchmark/training pipeline: benchmark/README.md
AGPL-3.0-only © Abel Vázquez Montoro