50,000 free calls a month, card-free. Get an API key →

news / adr-tunnel-compliance · raw .md
Compliance8 min readMapMap engineering
A cross-section of a road tunnel portal in engineering linework, with route lines fanning away and diverting around it under layered contour bands.

ADR tunnel compliance, enforced in costing

A tanker whose load carries ADR tunnel restriction code C/E may not pass through a category-D tunnel under the worst-case reading of ADR 8.6.4. MapMap knows that inside the cost model, before a route exists, so the geometry that comes back is one the vehicle can legally drive rather than one you have to check afterwards. We also sell the decision on its own: POST /adr/check returns the tunnel-entry verdict, with the rule that produced it, and no routing at all, at 1p per call. As of July 2026, MapMap is the only navigation API offering a self-serve ADR tunnel-compliance product at published per-call prices.

What does an ADR tunnel restriction code actually restrict?

It restricts which categories of tunnel a dangerous-goods load may pass through, and nothing else. Two attributes meet at the tunnel mouth. The tunnel has a category from A to E, assigned by the national competent authority and posted on the approach. The load has a restriction code from ADR Table A, column (15), and under ADR 8.6.3 the code of the whole load is the most restrictive code of any item on board.

The four unconditional codes are the simple case:

CodePassage forbidden through
BCategories B, C, D and E
CCategories C, D and E
DCategories D and E
ECategory E

The rest are conditional. B1000C bites at category B only above 1,000 kg total net explosive mass per transport unit. C5000D bites at category C only above 5,000 kg. The slashed codes (B/D, B/E, C/D, C/E, D/E) apply their first clause only when the goods travel in tanks, or in bulk in the case of D/E. A routing API does not know your net explosive mass and does not know whether today's movement is in tanks, so we read every conditional clause as applying. That collapses each code to a single threshold, which is its first letter: passage is forbidden when the tunnel category is greater than or equal to that threshold. The full 12-code table, including the ADR no-restriction entry, is on the API conventions page.

The direction of the error matters and it is deliberate. The worst-case reading can route a load around a tunnel it was in fact entitled to use. It will never send a load through a tunnel ADR could forbid.

Why we enforce the tunnel category in costing rather than post-filtering

Because post-filtering answers a question nobody asked. If you plan a route first and then scan it for forbidden tunnels, all you learn is whether that one route happens to be legal. When the answer is no, you plan again, and nothing about the second attempt makes it any more legal than the first. You end up looping, or you end up shipping a "compliance check" that is really a rejection notice.

We push the restriction down into the cost model instead. A tunnel the load cannot enter is not an edge the search can traverse, so the shortest legal path is simply what the search returns. There is no second pass. Either you get a route the vehicle can drive, or you get no route, and no route is honest information: it means the destination is not reachable under that load's restrictions and someone needs to make a decision a router cannot make.

Dimensional gates work at the same layer for the same reason. Height, width, length, weight and axle load are checked edge by edge against the OSM limits during the search, not against the finished line on the map. We wrote that up separately in truck routing and vehicle dimensions.

How do you get a compliance decision with no routing at all?

POST /adr/check takes a vehicle profile and a tunnel category and returns the ADR 8.6.4 entry decision. No coordinates, no geometry, no map. It exists because the compliance question and the routing question are genuinely separate, and plenty of the people who need the first one are not planning a journey: they are answering a question about a load, a tunnel and a rule.

bash
export BASE=https://api.mapmap.ai
export API_KEY=snk_...

curl -X POST "$BASE/adr/check" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "adr": {
      "dimensions": {
        "height_m": 4.0, "width_m": 2.55, "length_m": 16.5,
        "gross_weight_t": 44.0, "axle_load_t": null, "axle_count": null
      },
      "tunnel_code": "C/E",
      "hazmat": true
    },
    "tunnel_category": "D"
  }'

The response is two fields:

json
{
  "status": "blocked",
  "reason": "ADR 8.6.4: tunnel restriction code C/E forbids passage through tunnels of category D (worst-case reading; conditional clauses assumed to apply)"
}

A permitted load gets the short form, with no reason key at all:

json
{ "status": "allowed" }

tunnel_category accepts "A" through "E". tunnel_code accepts the canonical ADR notation and is nullable. hazmat and dimensions are required. The same decision is available to coding agents as the check_adr_tunnel tool on our MCP server.

Why a compliance decision you cannot explain is useless

Because the transport manager has to defend it, and "the API said no" is not a defence. When a load is turned back, or an insurer asks why a vehicle was on a particular road, or an auditor pulls six months of planning records, the person answering needs the rule, not the verdict. A boolean is not evidence.

So reason is a sentence with four things in it: the section of the agreement that applied (ADR 8.6.4), the restriction code that was in force (C/E), the tunnel category it was tested against (D), and the disclosure that this was the worst-case reading with conditional clauses assumed to apply. That last clause is the honest part. It tells a reader who knows their load better than we do exactly where our answer might be more conservative than the law requires, and it does so in the response rather than in a footnote on a docs page they will never read.

Log the string. It is written to be pasted into an audit trail and to still make sense to a human 18 months later.

The asymmetry is deliberate and worth naming: an allowed decision carries no reason, because there is no restriction to cite. If you need a positive record, record the request alongside the response.

How do you route the load rather than just check it?

Two ways, depending on which client you already have.

The compatible URL endpoint takes the load as vendor query parameters, so OSRM-shaped tooling can send them with no structural change:

bash
curl "$BASE/route/v1/truck/-0.1276,51.5072;-1.8904,52.4862?height=4.0&weight=44.0&hazmat=true&tunnel_code=D" \
  -H "Authorization: Bearer $API_KEY"
ParamUnitExample
heightmetresheight=4.0
widthmetreswidth=2.55
lengthmetreslength=16.5
weighttonnesweight=44.0
hazmatbooleanhazmat=true
tunnel_codeADR codetunnel_code=D

The native POST /route takes the whole load as a top-level adr object alongside a Valhalla-style body. dimensions, tunnel_code (nullable) and hazmat are all required keys, and the gateway merges them into costing_options.truck for you:

json
{
  "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": null, "axle_count": null
    },
    "tunnel_code": "C/E",
    "hazmat": true
  }
}

If your own costing_options.truck contradicts what the adr profile implies, that is a 400 costing-conflict with the offending fields listed, not a silent pick of one over the other. Three semantics are worth knowing before you send anything: "hazmat": false means the tunnel matrix does not apply at all (dimensional limits still do), "hazmat": true with no tunnel_code is treated as code B because the load's code is unknown, and the adr extension is honoured only on POST /route and POST /optimise.

What it costs

ADR compliance is our premium class, priced at 1p per call. That is 20 times the 0.05p standard rate, and we are not going to pretend otherwise: it is the thing we built, and it is worth more than a car route.

Monthly volumePremium price per call
Included free tier50,000 included calls, of which a premium call draws 20 (so 2,500 all-premium calls)
50,001 to 1,000,0001p
1,000,001 to 10,000,0000.7p
Beyond 10,000,0000.4p

Bands are marginal, so each price applies only to the calls that fall inside it. The premium class is selected automatically per request: /adr/check, "costing": "truck", a top-level adr object, "hazmat": true, or a truck profile or truck query parameter on the compatible endpoint. Everything else bills as standard. Payment is prepaid credit only, so there is no post-paid overage and no surprise invoice: with no credit you get a 402 or a 429. Prices reconcile with /pricing.json, which wins if this page ever drifts.

What this does not do

ADR is a legal framework and we implement one part of it: the tunnel-category entry decision defined in 8.6.4. We do not classify your goods, assign the restriction code, handle placarding, driver certification, national derogations or time-of-day tunnel orders. You give us the code, we give you the decision. The operator remains responsible for the movement.

Four more limits worth having in writing:

  • The worst-case reading is conservative on purpose. Quantity-conditional and tank/bulk-conditional codes are assumed to apply, so a load may be routed around a tunnel it was entitled to use. The error only ever runs in the safe direction, but it is a real error and it will cost you distance.
  • Tunnel categories come from OpenStreetMap tags. We read hazmat:adr_tunnel_cat, then hazmat:tunnel_cat, then infer from per-code hazmat:B=no style tagging on any way, tunnels and their signed approach corridors alike, including the hazmat=no plus hazmat:E=yes allowance style used on Dutch tunnel routes. A tunnel with no category tagged is treated as category A, no restriction. That is the correct legal default, since ADR restrictions apply where a category has been assigned and signed, but it does mean coverage depends on the data.
  • Tunnel-code 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 if you send one. Do not build a dangerous-goods compliance step on top of them.

Try it

Run POST /adr/check from the browser with no install at the playground, or read the request and response shapes in full in the API reference. A key comes from one card-free call to POST /v1/keys and works immediately.

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