# Measured clearance on a route

A truck router answers a height question from what the map records. Where
the map records nothing, it has nothing to act on, so it returns the road as
passable and the response looks exactly like one where every structure was
checked. That failure is silent, and it points the dangerous way: a missing
tag makes the answer look better than the ground.

`POST /v1/clearance/along` answers the other question. Given a route shape
and a vehicle, it walks the corridor over a survey's own geometry and reports
whether the vehicle passes, and if not, where, by how much, and with what
uncertainty. It reads geometry rather than attributes, so it is independent
of the tagging above.

Both answers stay true and neither replaces the other. The sentence our ADR
reference has carried since it shipped is the shortest way to say it: **an
unchanged route is not a clearance.** This page documents the measurement
that sentence tells you to go and get.

## Why the tagged answer is not enough

The numbers are our own, measured on one UK OpenStreetMap extract and
published with their denominators on
[truck restriction data, measured](/docs/truck-data-audit):

| | |
|---|---|
| Highway ways in the extract | 8,212,278 |
| Ways carrying `maxheight` | 18,353 |
| Ways carrying `maxheight:physical` | 107 |
| Height-tagged ways in one Cumbria sample | 0 of 893 major bridge ways |

Two consequences follow, and they are different from each other.

**A tag can be absent.** Absence of a recorded restriction is not evidence
that a road was checked. On this data it is the ordinary case.

**A tag describes a structure somebody signed.** `maxheight` is a posted
legal limit and `maxheight:physical` is a measured physical one, and there
are 107 of the latter in the whole extract. In practice neither describes the
branch of a mature street tree hanging over a lane, a temporary gantry, or a
service run slung under a deck. Those are real obstructions and nothing posts
a limit for them. Surveyed geometry finds them because it is not looking for
a sign.

**For context on why this matters:** the Network Rail struck-bridge dataset
runs to more than 5,000 structures, and 1,666 strikes were recorded in
2024/25, roughly one every five hours.

Turning survey data into gauging and clearance outputs is already bought at
scale, as a batch service delivered weeks later. Two published procurement
records, both about other organisations and neither one of ours: Network Rail
paid Cordel **£4,336,000** for point cloud data processing (Find a Tender
notice 030479-2026), and National Highways paid IBI Group **£556,560** for
"online visualisation and interaction with the data outputs with driven
imagery and LiDAR data" (Contracts Finder, expired February 2025). What is
documented below is the same class of work answered per request, against a
route.

## What the answer is, and is not

| It is | It is not |
|---|---|
| A physical measurement of the gap between a road surface and the lowest validated surface above it | A signed, posted or legal height |
| Taken from one dated survey, with a stated uncertainty bound | A certificate about the route |
| Reported per station along a corridor, with the limiting point named | A statement about signage, or about what you are permitted to do |
| Silent about ground the survey did not cover, and explicit that it is silent | An all-clear over ground nobody surveyed |

A posted height carries a margin somebody chose and it carries legal force. A
measurement carries neither. Every response therefore ships a
`clearance_enforcement` block whose `route_certified` field is always
`false`, and the field is a type rather than a boolean, so there is no value
of it that serialises to `true`.

Where a posted height and a measured gap disagree, that is a matter for the
road authority that posted the sign. It is not something a survey settles and
it is not something this endpoint edits.

## The two calls

Route acquisition is deliberately a separate call.

```bash
# 1. A truck route, so the search already avoids the restrictions the map
#    records. `geometries=polyline6` gives the six-digit shape the clearance
#    call wants; it comes back as routes[0].geometry.
curl "$BASE/route/v1/truck/-0.1276,51.5072;-1.8904,52.4862?height=4.2&width=2.55&weight=40&geometries=polyline6" \
  -H "Authorization: Bearer $API_KEY"

# 2. That shape, measured against the survey.
curl -X POST "$BASE/v1/clearance/along" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "geometry_polyline6": "u{~vFvyys@fS]...",
        "vehicle_height_m": 4.2,
        "vehicle_width_m": 2.55,
        "margin_m": 0.0
      }'
```

Two calls rather than one, because they are two different claims with two
different failure modes, and folding them together would produce one
answer that reads like both. Agents get the composition packaged as the
`check_clearance_on_route` [MCP tool](/docs/mcp), which makes the same two
calls and returns the measured answer with its bound.

### Request fields

| Field | Type | Meaning |
|---|---|---|
| `geometry_polyline6` | string, required | The corridor as a six-digit-precision encoded polyline, typically straight out of a prior route call: `routes[0].geometry` with `?geometries=polyline6` on the compatible URL endpoint, or `geometry_polyline6` from the `route` MCP tool. Capped at 10,000 vertices; fewer than two decoded points is a `400`. |
| `vehicle_height_m` | number, required | There is no default vehicle. |
| `vehicle_width_m` | number, optional | Asks the width axis as well. Without it the report answers for height and says why the other axis was not assessed. |
| `margin_m` | number, default `0` | Your operating margin, added to the vehicle before the verdict and echoed back. No compliance policy is baked in here: both figures are reported. |
| `datasets` | string array, optional | Narrows the search to named datasets, up to 16. It can never widen it. A name this key may not see, and a name that does not exist, are dropped identically, so the endpoint is not an existence oracle for anybody else's data. |

## The report

```json
{
  "basis": "surveyed_geometry_not_signage",
  "clearance_enforcement": {
    "basis": "surveyed_pointcloud_within_survey_difference",
    "route_certified": false,
    "vertical_datum": "WGS84 ellipsoidal heights, no undulation applied.",
    "caveat": "Headroom figures are measured from a dated survey's own geometry as a within-survey difference, with the stated uncertainty bound. They are not signed or legal clearances, the route is not certified, and ground without survey coverage is reported as not surveyed rather than clear. Verify signed restrictions before dispatching a load."
  },
  "route": { "distance_m": 1200.0, "geometry_polyline6": "u{~vFvyys@fS]..." },
  "vehicle": { "height_m": 4.2, "width_m": 2.55, "margin_m": 0.0 },
  "height": {
    "verdict": "pass",
    "coverage": { "kind": "complete", "assessed_m": 1200.0 },
    "tightest": {
      "location": { "lng": -0.1276, "lat": 51.5072 },
      "route_distance_m": 412.0,
      "clearance": {
        "headroom_m": 4.9,
        "sigma_m": 0.03,
        "sampling_gap_m": 0.11,
        "safe_headroom_m": 4.73,
        "overhead_class": "structure",
        "support_m2": 12.0,
        "surveyed_on": "2026-01-14"
      },
      "dataset": "acme-survey",
      "view_url": "https://mapmap.ai/demo/lidar/acme-survey?cam=...&pin=...&panel=clearance"
    }
  },
  "width": { "verdict": "pass", "coverage": { "kind": "complete", "assessed_m": 1200.0 } },
  "datasets": [
    { "dataset": "acme-survey", "survey_dates": "2026-01-12/2026-01-14", "stale": false }
  ],
  "advisories": [],
  "explanation": "The survey covers all 1200 m of this corridor."
}
```

`basis` and `clearance_enforcement.basis` are fixed strings, not free text.
`caveat` is a single constant defined once in the arithmetic crate and read
by the gateway, the MCP tool and this page, so the three cannot describe the
guarantee differently.

`view_url` is a deep link into the survey viewer, posed at the limiting
point. It is site-relative unless the deployment has been told its own public
viewer origin, because a safety report should not carry a guessed host.

`advisories` are notes attached to stretches of route: a vegetation-limited
reading, a survey age worth knowing about, a seam between two datasets.
`explanation` is one honest sentence for a human, generated rather than
written, and asserted in tests to be free of the words this product does not
get to use.

## The four verdicts

Both axes use the same ladder.

| `verdict` | What it says |
|---|---|
| `pass` | Every assessed station clears the vehicle by more than the stated bound, over coverage with no gaps. `tightest` names the closest point, and is absent where the survey found nothing above the corridor at all. |
| `fail` | At least one station does not clear the vehicle on the measured figure. `limiting` is the worst point and `all_failures` is every one of them, worst first, with `all_failures[0]` being `limiting` itself. |
| `indeterminate` | The vehicle clears the measurement but not the bound on it, or the deciding surface is foliage on a survey too old to trust. `reasons` carries machine-readable codes and `resolution_hint` says in one sentence what would settle it. |
| `no_verdict` | Coverage gaps prevented an answer. Not a fail, and emphatically not a pass. |

`reasons` on an `indeterminate` is one or more of
`inside_uncertainty_band`, `vegetation_age_exceeded`, `artefact_stale` and
`artefact_freshness_unchecked`. `no_verdict` carries `no_survey_coverage` or
`insufficient_data`. Branch on the codes and read the English to a person.

The ladder may over-restrict. It is built never to under-restrict.

## Every figure carries its bound

A clearance measurement has four numbers and none of them is optional.

| Field | What it is |
|---|---|
| `headroom_m` | What the survey measured: the lowest validated surface above the corridor, less the road surface under it. |
| `sigma_m` | Combined one-sigma measurement uncertainty, applied at two sigma. |
| `sampling_gap_m` | A one-sided bias bound. A gap between samples on the underside of a structure means the true low point can hang below the lowest sample anybody took, by roughly that much. |
| `safe_headroom_m` | `headroom_m` less two sigma less the sampling gap. This is the verdict input, and it is the figure to plan against. |

Quote `headroom_m` beside `safe_headroom_m` rather than on its own. A
headroom figure without its bound is a number somebody will treat as exact,
which is why the SDK's `formatClearance` takes the whole measurement rather
than a value and returns a branded `ClearanceText` a hand-built label cannot
be substituted for.

`overhead_class` is `structure`, `vegetation`, `wire` or `unknown`. Foliage
and wires are labelled, never dropped: a branch across a lane is a real
obstruction. Labelling them lets a reader know that a figure is seasonal and
compressible, or measured off a thin target, which is a different thing from
a figure off concrete.

`margin_m` is yours. It defaults to zero, it is added to the vehicle before
the verdict, and it is echoed back. It is applied to the measurement and not
to the routing step, where the map's posted heights already carry somebody
else's margin.

## Coverage is a type, not a caveat

Three states, and keeping the last two apart is the whole point.

- **Measured open sky.** The survey covered this ground, looked, and found no
  surface above the corridor. That is a measurement, and it supports a pass.
- **Not surveyed.** No survey covers this ground. It arrives in
  `coverage.not_surveyed` as located runs of route, and no verdict is drawn
  over it.
- **Insufficient data.** A survey reaches this ground but not well enough to
  decide on. It arrives separately, in `coverage.insufficient_data`, because
  the two have different remedies: one wants a survey and the other wants a
  better one.

"We measured no obstruction here" and "we have no data here" are different
answers, so `coverage` is two distinct shapes. The `complete` shape has **no
field for gaps at all**, and a `pass` verdict accepts only that shape. A
route that passes over unsurveyed ground is therefore a state the wire format
cannot represent, in TypeScript and in Rust alike, rather than a rule
somebody remembered to apply.

A refusal is not always thin coverage, either. The commonest reason a station
under a structure has no usable cell is occlusion: the survey could not see
through the thing above you. Neither the report nor this page words that as
sparse survey.

## Width is a different quantity

Supply `vehicle_width_m` and the report answers a second question: how wide
the clear corridor is. That answer arrives as `clear_width_m` and
`safe_clear_width_m` on a separate measurement type, never in a headroom
field, because a lateral distance read as a headroom is the same conflation
as a sign read as a measurement, turned through ninety degrees. The SDK keeps
them apart the same way: `MeasuredLateral` has no headroom field and
`formatLateral` will not take a `MeasuredClearance`.

The width axis answers `not_assessed`, with the reason spelled out, when no
`vehicle_width_m` was supplied, and again when the height axis could not be
assessed either. Where a width **was** supplied and the survey is too thin at
the corridor edges, the width axis runs the same ladder as the height axis
and comes back `indeterminate` or `no_verdict` over those stretches, while
the height axis can still answer for the same ground.

**Swept path is out of scope.** A route shape is a centreline approximation,
lane position is not recoverable from it, and overhang arithmetic on an
approximate centreline would manufacture precision. The corridor is sampled
across the vehicle's width plus a lateral allowance for the registration
error between a route shape and the carriageway it stands for. That
allowance covers the ordinary bow of the shape between its vertices. It does
not cover which lane a vehicle is in, and under an arch the outer lane is the
direction of danger, because a soffit is lowest at its edges. Lane-resolved
geometry fixes that and it is a later phase.

## The vertical datum, and why it cancels

`headroom_m` is `overhead_z - road_z`: two heights from the same cloud, the
same epoch and the same processing. A constant vertical offset, whether a
geoid undulation or a reheighting constant, cancels exactly in that
difference, so the absolute vertical basis never enters the arithmetic. What
is left is the differential survey error over the few metres between the road
and the structure above it.

A terrain model contributes nothing at any point. Not as a road surface, not
as a gap filler, not as an adjustment. A survey published as height above a
DEM is refused rather than caveated, because the disagreement between a
terrain model and a survey's own road surface is metre scale and is worst
exactly under the flyovers a clearance product exists to measure.

Every response names the frame its figures live in, in
`clearance_enforcement.vertical_datum`, spelled for a reader rather than as
an EPSG code.

## What it refuses to answer

Each of these is attempted by a test that asserts the refusal, rather than
being a promise on a page.

1. **A pass over unsurveyed or insufficient ground.** Structurally
   impossible, as above.
2. **A clearance without its uncertainty.** The measurement types require
   both bound fields.
3. **Legal or signed clearance questions.** Out of scope by type. The
   response text is swept for "legal", "compliant", "permitted" and
   "certified", with the enforcement caveat excluded by identity.
4. **Verdicts inside the uncertainty band.** `indeterminate`, with the
   shortfall stated and a hint at what would settle it.
5. **Vegetation-limited verdicts on stale surveys.** Past the vegetation age
   limit they degrade to `indeterminate` with the age given.
6. **Swept-path claims.** As above.
7. **Anything from a survey without a complete datum block.** Refused when
   the artefact is baked, so the runtime never sees it, and refused again at
   load if the block is doctored afterwards.

## Staleness, and what an operator sees

A clearance artefact records the sources it was baked from and their hashes.
The dataset manifest records what the dataset holds now. An artefact is fresh
only when every source still matches.

An artefact **nobody has checked** is treated exactly as one known to lag, so
a deployment that has not declared what its dataset holds gets the cautious
answer rather than the trusting one. Staleness never hides the artefact: the
report still carries its measurements, marks `stale: true` on the dataset
entry, and refuses `pass`.

`GET /admin/clearance` (admin token) is the operator view: every published
artefact, whether it still matches its sources, the bake's own quality
statistics, and the reason for any artefact that would not open. An artefact
that silently answers "not surveyed" because it is corrupt or oversized is
exactly what an operator needs to see.

## Accuracy, and why no headline figure appears here

There is no accuracy number on this page, and that is deliberate.

The bound is computed per measurement rather than assumed from a datasheet,
and it is computed from things that genuinely vary between one structure and
the next: the fit residual of the road estimate, the fit residual of the
overhead cluster, the differential georeferencing error of the survey, and
the sample spacing on the surface that is doing the limiting. A single
published figure would be read as a guarantee across surfaces, densities,
structure types and capture geometries it was never measured on, and the one
place it would be quoted is the place it would be wrong.

What is validated, and how, is worth stating plainly:

- **Synthetic scenes with known geometry.** A slab soffit at exactly 4.800 m,
  an arch whose minimum is off the centreline, a gantry with a catenary
  below it, a porous canopy, a density ramp down to the refusal thresholds,
  and injected birds and strays. The assertions are on the value, the bound,
  the class and every verdict transition.
- **Property tests.** More noise never moves an answer towards `pass`. Less
  density never moves one out of a refusal towards `pass`. Adding a constant
  to every height changes nothing at all, which is the datum argument above,
  mechanised.
- **Deliberate failure canaries.** Every check is also run against artefacts
  that are wrong on purpose, and each of those must fail. If one of them
  passes, the suite goes red on that alone, because at that moment the check
  has stopped being evidence. A clean log proves nothing on its own.
- **Independent re-measurement on real survey data**, by a different
  estimator that shares no code path with the bake. That bounds the
  estimator. It says nothing about the survey, because both readings come
  from the same returns, and the harness says so in as many words.

One known open question, stated because it is the kind of thing a page like
this usually leaves out. On a ragged underside, the overhead estimator can
read optimistically compared with an independent reading of the same points,
and how far is under active measurement rather than settled. That is one
reason the verdict ladder refuses rather than narrows, and one reason no
headline accuracy figure appears above.

Each baked field publishes its own quality block, and
`GET /admin/clearance` surfaces it. That block, and the checks above, are
what a validation conversation about a particular survey is held over. Ask
and we will walk you through them.

## Availability

Clearance artefacts live inside the hosted point-cloud dataset layout, so the
dataset manifest stays the single authority for who may see what:

```text
SN_POINTCLOUDS_DIR/
  acme-survey/
    dataset.json              manifest: access, plus the clearance block
    clearance/
      downtown.clr.bin        the field
      downtown.clr.json       its sidecar
```

| Variable | Default | What it does |
|---|---|---|
| `SN_POINTCLOUDS_DIR` | unset | The dataset directory. Unset, or set to a directory holding no clearance block anywhere, and `POST /v1/clearance/along` answers **501**. |
| `SN_CLEARANCE_VIEWER_URL` | unset | Public origin of the survey viewer, used to build `view_url`. Unset means site-relative links rather than a guessed host. |
| `SN_CLEARANCE_MAX_ARTEFACT_MIB` | `512` | Largest field the gateway will decode. A bigger artefact is refused rather than loaded: the corridor over it answers "not surveyed", which is never a pass, and the admin inventory names the file and its size. |

The 501 is computed across the whole deployment before any caller's
visibility is considered, so it can never become an oracle for whether
somebody else's data exists. Feature-detect on it, the same way clients
already do for elevation.

A clearance field is baked per dataset, offline, from the survey's own point
data, and it is separate work from a payload bake: it needs a declared
vertical datum, a class map it can trust, and a review of what the bake
refused. It is not part of the published point cloud bake packages. Talk to
us about the corridor or the network you want measured and we will scope it.

## In the SDK

`@mapmap/points` carries the wire types and two pure helpers, so a client can
hold a report without re-implementing anything safety critical:
`RouteClearanceReport`, `RouteVerdict`, `WidthVerdict`, `Coverage`,
`MeasuredClearance`, `MeasuredLateral`, `ClearanceEnforcement`,
`formatClearance`, `formatLateral` and `reportToGeoJson`. Nothing there
computes a clearance. Full list on the
[SDKs page](/docs/sdks#clearance-on-route).

## Next steps

- [Truck restriction data, measured](/docs/truck-data-audit): what the map
  actually records, with denominators
- [Truck and ADR routing](/docs/adr): which restrictions the router enforces,
  and where enforcement stops
- [API reference](/docs/api-reference#measured-clearance-along-a-route): the
  endpoint summary, and the OpenAPI document that is authoritative
- [MCP server](/docs/mcp): `check_clearance_on_route` for agents
