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

news / lane-guidance · raw .md
SDK6 min readMapMap engineering
A motorway approach splitting into five lanes, each with its own directional arrow, some crisp and others fading to dashed outlines where the data thins.

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:

BitIndicationBitIndication
1none64right
2straight128sharp right
4sharp left256uturn
8left512merge to left
16slight left1024merge to right
32slight 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:

LocationShapeCarries
bannerInstructions[].sub.components[]type: "lane" componentsdirections, active, active_direction
legs[].steps[].intersections[].lanesOSRM v5 Lane objectsindications 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.

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 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; for the enforcement argument that shapes the routes underneath, see truck routing with dimensions enforced in costing.

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