Truck routing with dimensions enforced in costing

# 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](/news/adr-tunnel-compliance).

## 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:

```bash
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`.

```json
{
"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 `truck` profile.** Send `height` on `driving` and you get a `400 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 `height` but omit `weight` and 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/D` must be encoded, which `--data-urlencode` handles. Setting `tunnel_code` also implies `hazmat=true` unless 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:

```bash
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:

```json
{
"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:

```json
{
"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_load` and `axle_count` are native-only. A request through `/route/v1/truck/...` leaves both unset, so axle-load gates do not apply. Use `POST /route` when 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/check` is always authoritative.
- **The analysis endpoints apply no ADR restrictions.** `/isochrone`, `/matrix`, `/trace_route` and `/trace_attributes` ignore an `adr` object 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`, always `400`), not RFC 9457 problem+json, because compatible clients dispatch on `code`. Auth, quota and payment errors keep the shared formats even on that path: `401` and `429` are problem+json, and beyond-tier `402`s use the x402 body. If your client dispatches on `urn: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](/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](/docs/api-reference), and the cross-cutting rules (coordinate order, units, quotas, the two `402`s and the full ADR tunnel-code table) live in the [API conventions](/docs/conventions).

Routing derives from OpenStreetMap, so credit "© OpenStreetMap contributors" when you render or republish it.