Truck routing with dimensions enforced in costing
MapMap routes trucks against real dimensional constraints: height, width, length and weight go into the cost model itself, so a 4.2 m trailer is never planned under a 4.0 m bridge and a 44 t artic is never sent down a lane posted at 7.5 t. There are two ways to send those numbers. The compatible URL endpoint GET /route/v1/{profile}/{coordinates} takes them as vendor query parameters, and the native POST /route takes them as costing_options.truck. Both return a route the vehicle can legally drive rather than a route you then have to check. Truck is the premium price class at 1p per call, selected automatically from the request.
Why dimensional limits have to be enforced in costing
Because a post-filter can only reject a route, it cannot find the legal one. If height is checked after planning, the engine returns the fastest path, your check finds a 3.9 m bridge on it at kilometre 140, and now you have nothing. You are holding a rejection, not a route.
The tempting fix is to delete the offending edge and re-plan. That gives you a loop with no termination guarantee, and each iteration only proves the route is legal against the specific constraints you happened to test on the specific edges you happened to look at. It is also usually wrong about the shape of the answer. The legal route for a 4.5 m vehicle is often not a small detour around one bridge. It is a different motorway junction 30 km earlier, because the low bridge sits on the only link off that corridor.
In costing, a dimension is an edge filter evaluated during the graph search. An edge is unusable when its posted legal limit is below the vehicle attribute:
| Vehicle attribute | OSM edge limit | Edge blocked when |
|---|---|---|
height_m | maxheight | edge max height < vehicle height |
width_m | maxwidth | edge max width < vehicle width |
length_m | maxlength | edge max length < vehicle length |
gross_weight_t | maxweight | edge max weight < vehicle gross weight |
axle_load_t (when known) | maxaxleload | edge max axle load < vehicle axle load |
The search never expands a blocked edge, so the first route that comes back is already legal, and it is the cheapest legal route rather than the cheapest route that happened to survive a filter. The same argument applies to ADR tunnel categories, which we make at greater length in ADR tunnel compliance, enforced in costing.
How do I send dimensions on the compatible URL endpoint?
As vendor query parameters on GET /route/v1/truck/{coordinates}, alongside the standard ones. Dover to Birmingham, 44 t artic at 4.0 m:
export BASE=https://api.mapmap.ai
export API_KEY=snk_...
curl -fsS -G "$BASE/route/v1/truck/1.3134,51.1279;-1.8904,52.4862" \
-H "Authorization: Bearer $API_KEY" \
--data-urlencode "overview=full" \
--data-urlencode "height=4.0" \
--data-urlencode "width=2.55" \
--data-urlencode "length=16.5" \
--data-urlencode "weight=44.0"
The vendor parameters, in full:
| Param | Unit | Example |
|---|---|---|
height | metres | height=4.0 |
width | metres | width=2.55 |
length | metres | length=16.5 |
weight | tonnes | weight=44.0 |
hazmat | boolean | hazmat=true |
tunnel_code | ADR 8.6.4 code | tunnel_code=D |
The response is the OSRM shape: distance in metres, duration in seconds, location arrays as [lon, lat], and geometry as an encoded polyline at precision 5 unless you ask for geometries=geojson.
{
"code": "Ok",
"routes": [
{
"distance": 362514.3,
"duration": 16043.9,
"weight": 16043.9,
"weight_name": "duration",
"geometry": "u{~vHmgg@…",
"legs": [
{ "distance": 362514.3, "duration": 16043.9, "summary": "M25, M40" }
]
}
],
"waypoints": [
{ "name": "A20", "location": [1.3134, 51.1279] },
{ "name": "", "location": [-1.8904, 52.4862] }
]
}
This is the point of the compatible endpoint. If your stack already speaks to an OSRM or Valhalla-style routing client, the change is a base URL, an Authorization: Bearer snk_… header and the truck parameters appended to a request you are already building. Coordinate order, units and response parsing stay exactly as they are.
Three things bite people:
- Truck parameters require the
truckprofile. Sendheightondrivingand you get a400 InvalidValue, not a silently ignored parameter. - Omitted dimensions are not unlimited. Send any one truck parameter and the rest fill from the EU 96/53/EC maxima for a standard artic: 4.0 m height, 2.55 m width, 16.5 m length, 40 t gross. Set
heightbut omitweightand you route as a 40 t vehicle, which is the safe default but is not your 3.5 t van. - Slashed tunnel codes need URL encoding.
tunnel_code=B/Dmust be encoded, which--data-urlencodehandles. Settingtunnel_codealso implieshazmat=trueunless you override it explicitly.
How do I send dimensions on the native JSON endpoint?
As costing_options.truck on POST /route, either directly or through the top-level adr object that the gateway merges into it. The adr form is the one to reach for, because it carries the tunnel semantics as well:
curl -fsS -X POST "$BASE/route" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"locations": [
{ "lat": 51.5072, "lon": -0.1276 },
{ "lat": 52.4862, "lon": -1.8904 }
],
"costing": "truck",
"adr": {
"dimensions": {
"height_m": 4.0, "width_m": 2.55, "length_m": 16.5,
"gross_weight_t": 44.0, "axle_load_t": 11.5, "axle_count": 5
},
"tunnel_code": "C/E",
"hazmat": true
}
}'
dimensions, tunnel_code (nullable) and hazmat are all required keys of the adr object. The gateway turns that into exactly this, which is what the routing engine sees:
{
"truck": {
"height": 4.0,
"width": 2.55,
"length": 16.5,
"weight": 44.0,
"axle_load": 11.5,
"axle_count": 5,
"hazmat": true,
"adr_tunnel_code": "C/E"
}
}
height, width and length are metres, weight and axle_load are metric tonnes, axle_count is an integer, hazmat is a boolean. axle_load and axle_count are emitted only when you supply them, so the engine's own defaults apply otherwise. You may set costing_options.truck yourself instead, but if your values contradict what the adr profile implies, the request is rejected with a 400 costing-conflict whose conflicting_fields names them. Set them in one place.
The native response is the Valhalla shape, with time in seconds, length in the request's units (kilometres unless you ask for miles) and shape as an encoded polyline at precision 6:
{
"trip": {
"legs": [
{ "summary": { "time": 8611.0, "length": 190.8 }, "shape": "_y|jbB_kiFc[?a[?…" }
],
"summary": { "time": 8611.0, "length": 190.8 },
"status": 0,
"status_message": "Found route between points",
"units": "kilometers",
"language": "en-US"
}
}
What does a truck route cost?
1p per call in the first paid band, because truck is the premium price class. Prices are tiered and marginal, so each band prices only the calls that fall inside it:
| Monthly volume | Premium (truck and ADR) | Standard (everything else) |
|---|---|---|
| Beyond the free tier, to 1M | 1p per call | 0.05p per call |
| 1M to 10M | 0.7p per call | 0.03p per call |
| Beyond 10M | 0.4p per call | 0.02p per call |
The class is decided from each request automatically, so there is no flag to set and no separate truck plan to buy. A call is premium when it uses "costing": "truck", a top-level adr object, "hazmat": true, POST /adr/check, or the truck profile or truck query parameters on the compatible endpoint. Everything else, including car and van routing, geocoding, matrix and isochrone, bills at the standard rate.
On the free tier, 50,000 included calls a month land after you verify an email, and commercial use is allowed. Included volume is denominated in standard calls, and a premium call draws 20 of them, matching the price ratio. All-premium free usage is therefore 2,500 truck calls a month. Above 25M calls a month we quote. Pay-as-you-go is prepaid credit only, a 4xx response is never charged, and no credit means a 402 or a 429 rather than a surprise invoice.
What this does not do
Dimensional enforcement is only as good as the map, and our map is OpenStreetMap. An edge with no limit tag is unrestricted for that dimension, so an unsurveyed low bridge carrying no maxheight will not block anything. This is a data-coverage limit rather than an engine limit, and we would rather you heard it from us than from a driver. If a bridge matters to your fleet, check it is tagged before you trust the route.
Four more limits worth knowing before you build against this:
- The compatible URL endpoint carries no axle detail.
axle_loadandaxle_countare native-only. A request through/route/v1/truck/...leaves both unset, so axle-load gates do not apply. UsePOST /routewhen they matter. - ADR tunnel-category enforcement needs an ADR-capable routing backend. Against a backend without ADR costing the dimensions still apply, but the tunnel restriction code is not enforced and there is no per-response warning field to tell you.
POST /adr/checkis always authoritative. - The analysis endpoints apply no ADR restrictions.
/isochrone,/matrix,/trace_routeand/trace_attributesignore anadrobject sent to them. Do not use them for dangerous-goods compliance. - The compatible endpoint uses its own error envelope. Routing errors there are OSRM-shaped
{"code": ..., "message": ...}bodies (InvalidQuery,InvalidValue,NoRoute,NoSegment, always400), not RFC 9457 problem+json, because compatible clients dispatch oncode. Auth, quota and payment errors keep the shared formats even on that path:401and429are problem+json, and beyond-tier402s use the x402 body. If your client dispatches onurn:sn-gateway:problem:type URNs, it needs one extra branch here.
Try it
Run a truck route from your browser with no install at the playground, or issue a key with one card-free call and paste the curl above. The truck parameters, error slugs and units on this page are specified in the API reference, and the cross-cutting rules (coordinate order, units, quotas, the two 402s and the full ADR tunnel-code table) live in the API conventions.
Routing derives from OpenStreetMap, so credit "© OpenStreetMap contributors" when you render or republish it.
