Lane diagrams from OSM turn:lanes, and when we omit them

# Lane diagrams from OSM turn:lanes, and when we omit them

MapMap renders lane diagrams in banner instructions from OpenStreetMap `turn:lanes` data, and draws nothing at all when that data is absent. Ask for them with `steps=true&banner_instructions=true` on `GET /route/v1/{profile}/{coordinates}` and each step gains a Mapbox-shaped `bannerInstructions` array whose `sub` line carries one `lane` component per physical lane on the approach. Where OSM has no lane tagging, the `sub` line is omitted entirely rather than filled with a guess. That is the whole design: a lane diagram is only worth showing if it is right, because a driver at 70 mph reads it in about the time it takes to blink and cannot audit it.

## How do I get lane diagrams on a route?

Set `steps=true` and `banner_instructions=true`. Banner generation is gated on `steps=true` in the gateway, because a banner describes a step and there is nothing to attach it to otherwise. `voice_instructions` and `language` are independent switches on the same endpoint.

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

curl -fsS -H "Authorization: Bearer $API_KEY" \
"$BASE/route/v1/driving/-0.1278,51.5074;-0.0877,51.5099?steps=true&banner_instructions=true&language=en-GB" \
| jq '.routes[0].legs[0].steps[0].bannerInstructions[0]'
```

A step whose upcoming manoeuvre has lane data comes back like this:

```json
{
"distanceAlongGeometry": 412.3,
"primary": {
"text": "Upper Thames Street",
"type": "turn",
"modifier": "right",
"components": [
{ "type": "text", "text": "Upper Thames Street" }
]
},
"sub": {
"text": "",
"components": [
{ "type": "lane", "text": "", "directions": ["sharp left", "left"], "active": false },
{ "type": "lane", "text": "", "directions": ["left"], "active": false },
{ "type": "lane", "text": "", "directions": ["straight", "right"], "active": true, "active_direction": "straight" }
]
}
}
```

Three physical lanes, in road order, left to right. The third is the one to be in, and `active_direction` says which of its two permitted movements the route actually takes. Every lane component carries `active`, so read it as a boolean rather than testing for the key's presence. `active_direction` appears only on lanes whose recommended direction resolves to a single indication, so treat that key as optional. A step with no lane data has no `sub` key.

## Why is Valhalla's lane data not the shape the SDK wants?

Because Valhalla emits direction bitmasks and the OSRM/Mapbox banner model wants per-lane booleans. Valhalla's `turn_lanes()` serialiser writes `directions` as a lane's full bitmask, then at most one of `active` or `valid`, each holding a second bitmask naming the single direction the manoeuvre would use. Neither field is ever a boolean, and a lane in the invalid state carries neither field. The mask values come from `baldr/turnlanes.h`:

| Bit | Indication | Bit | Indication |
|---|---|---|---|
| 1 | `none` | 64 | `right` |
| 2 | `straight` | 128 | `sharp right` |
| 4 | `sharp left` | 256 | `uturn` |
| 8 | `left` | 512 | `merge to left` |
| 16 | `slight left` | 1024 | `merge to right` |
| 32 | `slight right` | | |

So `{"directions": 66, "active": 2}` means a lane marked straight-or-right, where you should be, going straight. Our translation layer decodes that into `directions: ["straight", "right"]`, `active: true`, `active_direction: "straight"`. The derivation rules are exactly three: `active` is true when the active mask is non-zero, `valid` is true when the valid mask is non-zero **or** the lane is active (an active lane is by definition usable), and `active_direction` is set only when the mask decodes to exactly one known indication. Boolean inputs are still accepted, for pre-normalised payloads, in which case there is no direction information to recover and `active_direction` stays absent.

## Why does the same lane data appear in two places?

Because banners and route analysis want different things, and we did not want to overload one field with both. The lane set is emitted twice per step:

| Location | Shape | Carries |
|---|---|---|
| `bannerInstructions[].sub.components[]` | `type: "lane"` components | `directions`, `active`, `active_direction` |
| `legs[].steps[].intersections[].lanes` | OSRM v5 Lane objects | `indications` and `valid` from OSRM v5, plus `active` as our extension |

The banner drops the per-lane `valid` flag. That is deliberate, and it is the most important honest detail on this page. A banner is a display surface: it has room to say "be here", and that is `active`. It does not have room to say "this other lane is legal for your manoeuvre but you would then need to change lanes", which is what `valid` without `active` actually means. Rather than let a UI infer that a non-recommended lane is illegal, we do not put the flag in the banner at all. The web SDK's `bannerLanes()` mirrors this: it returns `valid: true` for active lanes and leaves `valid` **undefined** for the rest, never `false`. If you need the real legality set, read `route.raw` at `legs[].steps[].intersections[].lanes`. That array is built from the same lane entries but does not depend on `banner_instructions`, so `steps=true` on its own is enough to get the legality data without any banner.

## What is turn:lanes coverage in OSM actually like?

Uneven, and we are not going to give you a headline percentage, because any single number would be a lie about a dataset that varies by country, by road class and by which mapper last touched the junction. The honest description: `turn:lanes` is a voluntary tag. Motorway junctions in well-mapped parts of Europe often carry it. A three-lane approach to a suburban roundabout often does not, even when the paint on the road is unambiguous. Coverage on your specific corridor is an empirical question, and the only correct answer is to route it and look.

The consequence that matters is that **absence of `turn:lanes` is not evidence of absence of lanes**. An untagged junction and a genuinely single-lane junction are indistinguishable in the data. This rules out the tempting fallback of synthesising a diagram from the `lanes` count or from the manoeuvre modifier, because it would render a confident diagram from an inference, and a driver has no way to tell an inferred diagram from a surveyed one. So we do not. Our lane handling is strictly additive: unparseable lane JSON yields no entries rather than an error, and no entries yields no `sub` banner. Guidance degrades to a primary line with a manoeuvre arrow, which is what every other navigation product shows anyway, and it is honest.

One thing we do rather than drop: a lane whose `directions` field is malformed keeps its slot in the array with an empty `directions` list. Dropping it would silently shift every other arrow one position left, and a diagram that is misaligned with the paint on the road is worse than no diagram. Losing one arrow is recoverable. Lying about which lane is which is not.

## What it costs, and what it does not do

Lane diagrams are not separately billed. They are a field on a route response, so a car route with banners is one standard call at 0.05p, and a truck route with banners is one premium call at 1p. Prices are on [pricing.json](https://mapmap.ai/pricing.json).

The real limits, plainly:

- **No coverage guarantee.** If OSM has no `turn:lanes` on your junction, you get no diagram, and we cannot fix that from our side. You can fix it upstream by tagging the junction in OSM, and it will reach you on the next territory build.
- **Banners omit `valid`.** Non-recommended lanes report `active: false` and unknown legality. The full set is in `intersections[].lanes`.
- **`active_direction` needs a single-bit mask.** A boolean-shaped input, or a mask naming two directions, produces no `active_direction`.
- **The built-in banner is a fallback.** `GuidanceBanner` draws Unicode arrows via `directionArrow()`. Sprite-based lane icons ship with the map assets, and serious navigation UI should use those. Set `showLanes: false` in the nav design block to suppress the lane row.
- **Lane data comes from the route, not from your position.** We tell you which lane the manoeuvre uses. We do not detect which lane you are currently in.

## Try it

The [web maps SDK](/docs/sdks) reads all of this for you: `extractGuidance(route)` returns one `StepGuidance` per step, `bannerLanes(banner)` gives the typed lane array, and `GuidanceBanner` renders it. The same `@mapmap/core` guidance state machine runs on device for the offline path. For how the voice and banner layer is built and when each instruction fires, see [turn-by-turn instructions](/news/turn-by-turn-instructions); for the enforcement argument that shapes the routes underneath, see [truck routing with dimensions enforced in costing](/news/truck-routing-dimensions).

Lane data derives from OpenStreetMap. Credit "© OpenStreetMap contributors" when you render it.