Junction Structures
Junction Structures
Return the complete, un-clipped centerlines of bridges/overpasses and tunnels/underpasses near a point. This powers Navvo's 3D junction view during turn-by-turn navigation — elevated viaducts on piers, and recessed tunnel portals — but it is useful for any client that needs grade-separation geometry for a small area.
Scope required: geo
Preview a junction in 3D
Open the junction preview to inspect a bridge or tunnel. The preview adapts to desktop and mobile screens and offers Overview and Drive controls to show the crossing and follow the route.
The scene uses lighting aligned with its vertical axis, distinct roof and wall shading, raised bridge parapets, and softer shadows that follow the deck's curves. A stronger route colour keeps the driving path visible against the roads. The overview now follows the chosen bearing at an oblique angle, while tunnel wall joints and paired ceiling light strips make depth easier to judge. With prefers-reduced-motion, route chevrons remain still.
Loading, unavailable geometry, and request errors have separate states. For integrations, available: false means geometry could not be obtained; it does not establish that no bridge or tunnel exists there. A successful response with available: true and empty structure arrays means no matching structures were returned. Show a retryable unavailable state when a standalone preview cannot obtain geometry, and keep normal navigation usable.
GET /junction/structures
Return every bridge and tunnel highway within radius metres of lat,lng.
Query parameters
320, clamped to 80–800.curl -s "https://navvo.io/api/v1/junction/structures?lat=31.98382&lng=35.89252&radius=320" \
-H "x-api-key: nvvo_YOUR_KEY"
Response
{
"source": "navvo-junction",
"available": true,
"bridges": [
{
"id": 150918346,
"kind": "bridge",
"roadClass": "secondary",
"layer": 1,
"name": "شارع الشهيد وصفي التل",
"names": { "en": "Wasfi Al-Tall Street", "ar": "شارع الشهيد وصفي التل" },
"oneway": true,
"lanes": 2,
"coordinates": [[35.8923719, 31.9831129], [35.892758, 31.9829988], "…"]
}
],
"tunnels": [],
"roads": [
{
"id": 42117903,
"kind": "road",
"roadClass": "primary",
"layer": 0,
"name": "شارع الملك عبدالله الثاني",
"oneway": false,
"lanes": 3,
"coordinates": [[35.8919, 31.9836], [35.8925, 31.9833], "…"]
}
],
"buildings": [
{
"id": 88213004,
"height": 12.8,
"coordinates": [[35.8921, 31.9838], [35.8924, 31.9838], [35.8924, 31.9835], [35.8921, 31.9838]]
}
]
}
| Field | Type | Description |
|---|---|---|
source | string | Always navvo-junction. |
available | boolean | false when geometry is temporarily unavailable. This differs from a successful response with empty arrays; clients should fall back gracefully. |
bridges / tunnels | array | The structures of each kind near the point. |
buildings | array | Building footprints within ~360 m of the point, nearest first, capped at 900. Context mass for a 3D junction view — enough to put the whole manoeuvre inside its city block rather than in open ground. |
…buildings[].height | number | Metres. From OSM height, else building:levels × 3.2, else a per-class default (house 5, apartments 14, commercial 12, otherwise 9) — most buildings carry no height tag at all, and omitting them leaves a junction floating in empty space. |
…buildings[].coordinates | array | Closed [lng, lat] ring. May be self-intersecting in raw OSM data — guard your extrusion. |
roads | array | Surrounding at-grade carriageways within the radius (motorway…residential, plus link roads), capped at 400 ways. Context only: these are what the structure crosses. Entries share the structure shape but always carry kind: "road". |
…[].id | number | Navvo map-data way id. |
…[].kind | string | bridge or tunnel. |
…[].roadClass | string | OSM highway class (motorway, trunk, primary, secondary, …) — use it to derive road width. |
…[].layer | number | Vertical ordering (OSM layer): >0 is above ground, <0 below. |
…[].name | string | null | Street name, if known. |
…[].names | object | Language-tagged names from name:<code> — en, ar, fr, es, tr, de — for whichever are mapped. Render signs and labels in the reader's own language from this rather than from name, which is whatever the local mapper wrote. |
…[].oneway | boolean | Whether the carriageway is one-way (dual carriageways arrive as two ways). |
…[].lanes | number | null | Lane count, if tagged. |
…[].coordinates | array | Full [lng, lat] centerline of the structure (a single continuous way). |
roads returns ordinary carriageways alongside the structures, so test kind explicitly — treating every non-tunnel entry as a bridge will sprout decks from plain roads.- Measure against segments, not vertices. To decide whether a route point is on a structure, use point-to-segment distance. Way vertices sit tens of metres apart, so a route running down the middle of a structure can be far from every vertex and be misclassified as at-grade.
- Ramp in metres, not in vertices. Height must reach grade over a fixed approach length (~55 m works well) using arc length along the route. A vertex-count smoother produces a 5 m ramp in one place and a 60 m one in another from identical geometry. Keep full height inside the structure — filtering across the whole profile sags the deck.
- A bridge needs approach embankments. A deck that starts at full height with a vertical support under each end reads as a cliff, not a ramp. Build an earthwork that rises from grade to deck level, wider at the base than the crest.
- A tunnel needs the ground CUT OPEN. Nothing below grade is visible through an unbroken ground plane — drawing a bore beneath a solid ground polygon renders nothing at all. Punch a hole along the below-grade span, add a floor and side walls, and cover only the middle with a translucent overburden so the route still reads underneath.
- Portals are frames, not slabs. Give the portal an arched opening so the route is seen through it; a solid headwall makes the route appear to stop dead.
- Exaggerate height. True clearance (~5.5 m) is a few pixels at any framing that also shows the junction. Navvo draws ~13 m per layer level.
- Draw the driver's own carriageway. From a bird's-eye a bare route ribbon suffices; from the driver's seat it does not. Without a road surface, edge lines and lane dashes the eye has nothing to judge speed, width or lane position against.
- Buildings are not decoration. They supply scale, parallax and a horizon to measure the structure against; without them the view reads as a test harness.
- Frame the structure, not the route. A real tunnel can be 50 m inside a 600 m route corridor; fitting the whole route shrinks the one feature the view exists to explain.
- Centre your context fetch on the JUNCTION, not on the coordinate you searched with.
lat,lngfinds the nearest structure of the requested kind, and that structure can be hundreds of metres away — at one Amman portal the resolved structure sat 280 m from the search point, so the nearest lane surface in the payload was 106 m from everything being drawn and the roundabout the driver was about to enter was missing entirely. Resolve the structure first, then re-fetchstructuresandstreetsaround it. - Never let a richer layer silently replace a simpler one it cannot cover. Lane surface should only take over from road outlines where it actually reaches the manoeuvre; otherwise a partial payload renders as empty ground, which looks like a working view of nothing rather than an obvious failure.
- Cap building height. Buildings are true-scale while grade separation is exaggerated, so one genuinely tall tower can hide the subject — a 105 m block 118 m from a junction covered a third of the frame. Context supplies scale and parallax; it should never occlude the manoeuvre.
- Size the frame from the manoeuvre, not from the whole window. Letting the exit tail count inflated one loop's extent from ~120 m to 173 m, opening the frame to its cap and leaving the manoeuvre small in dead ground. The driver is not asking where the road goes after the manoeuvre ends.
- Assert composition, not just that it rendered. A scene can build cleanly and still be a picture of the wrong place. Check a measurable property — e.g. that lane geometry exists within the junction itself — or a silent data gap will pass every "it rendered" test.
- Sub-pixel detail is noise, not detail. Lane lines drawn at ~1 screen pixel and painted areas (dashes, arrows, give-way triangles) drawn at true ground size become a third of a pixel each once the frame opens to ~350 m. A few thousand of them turn every road — and any deck carrying them — into a speckled haze that reads as a broken texture. Give lines a real pixel width (~2.2 px) and DROP painted areas entirely above a metres-per-pixel threshold: an arrow that cannot be recognised as an arrow is grain.
- But gate on the TIGHTEST scale the camera reaches, not the widest. This rule is about what can be seen, and it silently becomes a rule about what gets built the moment the camera can move. With a single wide framing at 0.64–0.90 m/px, the threshold suppressed every painted marking and the modelled deck — measured
paintTris: 0,decks3d: 0— so a view that later moved closer would have had nothing to reveal. Size geometry from the closest shot and hide the fine group by camera distance at draw time; geometry that is never built cannot be brought back. - The same applies to imported geometry. A modular deck module carrying railings, ribs and a parapet, tiled 35 times and seen nearly edge-on, moirés into a cross-hatch that reads worse than a plain extruded deck. Use the detailed model only where its detail resolves, and fall back to flat surfaces plus one fascia and one edge line above that.
- Take the FORM from a model and the COLOUR from your palette. Junction view is a flat-shaded diagram, not a render. Photographic concrete and asphalt at junction scale is fine-grained noise; override the imported materials with the same palette the procedural geometry uses and the model still contributes everything you imported it for.
- Size the portal from the road, not from a constant. A fixed 16 m half-width put a 32 m arch over a 7 m street, so the portal read as a monument rather than a hole the road goes into. Derive it from the carriageway (~0.78×) with a floor of about 6 m, below which an arch stops being legible at a junction-wide frame.
- The two requirements are sequential, not simultaneous. Trying to satisfy both at once oscillates: widen the frame and the carriageway becomes a thread with every marking auto-suppressed; tighten it and half the manoeuvre falls outside. Cut the view into shots instead — establish the junction wide, descend onto the approach, travel the manoeuvre at lane scale, then resolve on the exit.
- Ride an arc-length parameterised curve, not the raw vertices. Build the camera path from the driving line (route + lane offset + solved elevation) as a centripetal Catmull-Rom spline: on a loop ramp the path doubles back with vertices bunched at the apex, and uniform or chordal parameterisation overshoots into cusps exactly there, swinging the camera wide of the road. Then traverse it by arc length — parameter space races across sparse motorway vertices and crawls through dense ramp ones, which reads as the camera speeding up and slowing down for no reason.
- Clamp the curve parameter yourself. A chase camera samples behind and ahead of the vehicle, so it asks for positions off both ends of the curve on the first and last frames of the travel shot. Catmull-Rom sampling does not clamp: out-of-range input walks off the arc-length table, produces NaN, and then indexes an undefined control point — it throws rather than returning something harmless.
- Use a trapezoidal speed profile, not a single ease over the whole shot. Easing the entire travel makes the middle of the manoeuvre the fastest part, which is the one place the driver needs time to read. Ramp in, hold a constant speed through the junction, ramp out.
- Hand each shot over at the pose the previous one actually ended on. A slow push during the establishing shot that the next shot starts from the original pose throws the camera backwards at the cut — with a 7% push that is 12 m at a 150 m standoff and 28 m at 350 m.
- Keep the eye above grade even where the route is not. In a cutting the road falls ~8 m below the surface; an eye pinned to the road ends up inside the earth looking at the inside of a trench wall. Take the highest of the local heights and floor it at grade, so the camera looks down into the tunnel approach and the portal stays in shot.
- Size the lane-surface request to the route, not to the frame. A frame-sized radius is correct only while the camera stands still. Once it travels several hundred metres of route, everything it passes is drawable, and a frame-sized payload leaves the route crossing bare ground with no asphalt under it.
- Keep one cycle under 12 seconds. NHTSA's Visual-Manual Driver Distraction Guidelines set a 12.0 s total-eyes-off-road acceptance criterion, and a looping animation has no natural end — a driver waiting to "see the whole thing" commits to a full cycle. Keep individual shots near the companion 2.0 s single-glance criterion too, so any one glance lands on a complete, readable beat.
- Offer a still. Honour
prefers-reduced-motionby holding the flight at its most informative pose rather than disabling the view. Keep route chevrons still too, so the reduced-motion view does not retain a continuously moving overlay. Held at the start of the travel shot it is close to the junction with the approach still in frame and all the lane detail resolved — strictly more informative than the wide shot it replaces.
- A way does not have one height. Taking a single mean height per way and paving every polygon of it at that height turns a bridge approach climbing 0→13 m into a flat slab at ~6.5 m: it steps up out of the ground at the bottom, and the deck steps up out of it at the top. Sample the height where each polygon actually sits — project the vertex onto the way's own centreline and interpolate — and keep the fast constant path for ways that are genuinely level, so only the ways that climb pay for climbing.
- Generate the driven carriageway from the ROUTE, not from the way. Every other surface is keyed to a way's solved height while the route carries its own per-vertex profile, and on any ramp those two disagree by design. A carriageway derived from the route cannot disagree with it, and it guarantees the one road that must never be missing — the one being driven — is always there.
- Never let a richer layer blank a simpler one, and do not decide that per way. Lane geometry does not tile continuously. Drawing outline ribbons only for ways the lane surface "fails to cover", judged by a coverage fraction, does not remove holes — a way that is 70% covered counts as covered and gets nothing, so the other 30% stays bare. Draw the ribbon network for every way as a continuous underlay beneath the lane surface, in the same asphalt tones, so a hole is invisible rather than a coloured patch.
- Cut and cover, not a constant-height roof. A soffit at road + 6.4 m is only a tunnel where the road is more than 6.4 m down; over an approach ramp 2.5 m below grade the same soffit lands 4.7 m above the ground and renders as a ridge erupting from the landscape. Cap the roof just under the ground plane and build it only where that still leaves headroom. The right structure falls out for free: an open cutting on the approaches, a covered bore where the ground is deep enough to cover it.
- Occlude the route, don't delete it. Drawing one ordinary depth-tested ribbon over the whole length — including through the bore — gives both behaviours at once with no per-frame test: solid ground hides the buried span from above, so the line still stops at the portal, and inside the tunnel the same geometry is simply there ahead of the camera. Cutting it into runs is what produces disconnected stubs and, seen near end-on, a pale smear across the frame.
- Cartographic surfaces should be front-facing. Tarmac, paint and ground are lids on the world. Double-siding them is invisible until a camera goes below them, at which point it renders the underside of every street above — pale slabs and mirrored lane markings filling the frame and hiding the tunnel you are in.
- Carve the open cutting, or the road does not enter a tunnel — the camera just goes underground. The descending carriageway, its retaining walls and the portal can all be built correctly and still be invisible, because they sit beneath an unbroken ground plane. From above that reads as flat ground, then a viewpoint inexplicably below it, then flat ground again. Punch the hole along the open cutting only; the covered bore keeps its ground, because that ground is the lid the surface roads run across — carving it is what leaves a roundabout floating over a trench.
- Carve in two pieces. A ~60 m hole punched directly into a 4 km ground plane is what makes the triangulator throw, and since one exception aborts the whole build it blanks the entire scene. Use an inner patch (~1.2 km) that carries the corridor holes plus an outer ring — a square hole in a square, trivial to triangulate — to reach the horizon. Make a failed carve loud: a silent fallback to an uncarved plane looks exactly like "the tunnel renders wrong".
- Mitre the join, or every corner pinches. Offsetting a polyline by a constant half-width is the obvious way to thicken it and it is wrong at every vertex: on the outside of a bend the two adjacent offset lines meet further out than half-width, so a constant offset pulls the outer edge in and leaves a notch. Divide by the cosine of the half-turn, and cap it — that factor runs to infinity as a join approaches a hairpin and will fire a spike across the scene; past the cap, degrade to the plain offset, which is a bevel.
- Mitring is not smoothing. It fixes the notch but cannot make a polygon round. A route resampled at 6 m is a chain of 6 m chords, so a bend still reads as a series of flats. Refit the path through a centripetal Catmull-Rom and resample at ~2 m before thickening it — and round the road underneath with the same call, or the tarmac corners while the ribbon curves and the two peel apart at every bend.
- Open on the road, not on the junction. A wide establishing shot shows the manoeuvre from a distance the driver never sees it from. Starting on the carriageway a fixed run before the route begins — extrapolating backwards along the opening heading for the arc lengths that precede it — reads as driving, and it deletes the shot boundaries that a blended camera has to stop at.
- Smooth anything the camera derives from geometry. Look-ahead tied to a raw per-sample corner radius steps from frame to frame wherever the road curves, and it reads exactly as the camera stuttering into each bend. Average the radius over ~30 m so the derived quantity is continuous in position.
- Bound the gradient, and you stop chasing spikes. Elevation solved per node does not know its values must be joined by a driveable surface; at a two-level interchange it can return 0, 26 and 0 within a few metres, which renders as an inverted-V cliff through the carriageway. Rather than special-case each configuration, slope-limit both the route profile and every road profile with two passes (forward, then backward). Set the limit against the DIAGRAM's own scale, not the real world — grade separation drawn at ~13 m per level over a ~55 m ramp is already ~24%, so ~34% admits everything the view intends and still rejects a 13 m step over one resample interval, which is 216%.
- A covered bore needs real clearance, not merely a lid that fits. If any shallow dip counts as covered, a road 4 m down gets a 3 m "tunnel" — and a camera flying into it fills the frame with soffit. Require roughly the clearance a real road tunnel carries (~5 m); below that it is an open cutting, which is truer and is a shape the camera can descend into.
- Keep signs out of cuttings and bores. A gantry hung over a tunnel mouth sits directly in the camera's path — the eye drives into the back of the panel and the frame goes green. The portal is what names that stretch of road.
- Tile a deck along the slope, and TIP each tile to it. A module yawed to face along the road but never pitched stays horizontal at its own mid-height; tiled down a ramp that is a flight of stairs. Scale its length along the slope rather than the horizontal shadow too, or tiles fall short on a steep ramp and open gaps. Track the gradient the geometry demands separately from the pitch actually applied — they agree by construction, which is exactly what makes the pair useful as a regression check.
- "Does this way vary?" is the wrong question; "is it off grade?" is the right one. Gating per-vertex heights on a way's own height range re-creates the staircase, because a long viaduct is split into many short ways that are each individually almost level. Give per-vertex heights to everything that is elevated at all, and keep the constant fast path only for ways genuinely at grade.
- Derive a portal downward from the ground, not upward from its arch. Sizing a headwall as
arch crown + marginputs it above the surface on any shallow bore — a concrete lump in the grass at both ends. Fix the top just under the ground plane and size the arch to fit beneath it, and nothing protrudes at any trench width or depth. - A way's height belongs near that way. Lane geometry matched to a road by id and sampled against its centreline is right for the carriageway and wrong for everything else in the same feature: an intersection surface where a viaduct meets ground roads spans tens of metres and gets lifted wholesale to deck height. Fade the lift out with distance from the centreline, and give a surface with no road id the height of whichever road it actually adjoins rather than a constant from its layer tag.
- A rotation sign is not guessable — measure it. Rotating a forward
+Xabout the LEFT-of-travel axis by a POSITIVE angle pitches the nose DOWN, so a climbing tile must be rotated by the NEGATED gradient. Get it backwards and the deck still reads as climbing, because the tile centres still follow the profile — only each slab leans the wrong way, and the tell is that the tile's forward axis lands at twice the gradient away from the centreline. Have each tile compare its own world forward axis against the direction it climbs; nothing else catches a sign flip. - A guard whose branches collapse to the same expression is not a guard. One written to catch this flip read
|rot.x − π/2| > 1e-6 || |atan2(dz,h)| > 0 ? |atan2(dz,h)| : 0— both sides of the||reduce to the same value, so it could never have failed. Assert the property, not the presence of a computation. - Never let an OSM way's digitisation order decide which way something faces. Coordinate order is arbitrary and is not travel direction, so a panel oriented from it faces backwards on roughly half of all ways. With a double-sided material that does not hide the panel — it renders the legend MIRRORED. Orient from the approach heading and flip when the normal points away.
- Check that a structure's members agree with each other. A gantry's legs on the road normal and its beam on the road tangent are each individually defensible and jointly a floating cross that touches neither leg.
- Weld a structure back together before you build it. A viaduct is split at every tagging boundary, so one bridge arrives as several ways. Build a deck per way and the join shows twice over: the tile run restarts (any overlap that hides joints applies only within a way), and the width jumps because each way carries its own road class. Chain ways that share an endpoint node into one centreline first — the same topological test used elsewhere — and let the widest member set the width, because a deck that narrows halfway across is the artefact you are removing.
- A displaced shadow must follow the LOCAL height, and be capped. Offsetting a cast shadow by the structure's maximum height and applying that to every point works only while decks are short. Once whole structures are drawn, one containing a tall section throws its entire shadow sideways — including under the parts that are barely off the ground — and what is left is a grey slab in open ground with no bridge above it. Past a certain offset a shadow stops reading as height and starts reading as a separate surface.
- Isolate by hiding, but know what your switches are wired to. Hiding one layer to see whether it is the culprit only works if that switch does not also disable another: gating the lane surface on
roads && streetsmeans hidingroadssilently removes the lane surface too, and an "empty" result proves nothing. - Two systems must never pave the same deck. An imported deck module carries its own road face; lane geometry draws one too. Coplanar, they do not resolve — the depth buffer picks a winner per pixel and the deck comes out mottled into patches of each. Decide which layer owns the surface and switch the other off where it is covered, keeping it as the fallback where the payload does not reach.
- Colour-code your layers before you guess again. If two or three attempts at "which layer draws this?" have each produced a small change that looked like progress, stop patching and tint every contributor a distinct flat colour. One screenshot then names the layer under any pixel; the alternative is an unbounded sequence of plausible fixes.
- An oversized fallback is free on the ground and not in the air. Drawing a simple road outline under every road is the right way to stop a gap in lane geometry leaving the route on bare earth — at grade it is slightly wider than the real geometry but lies on the ground, invisible where covered. Elevated, the same ribbon overhangs the structure as a lip of tarmac in mid-air and fights the surface a few centimetres above it. Keep the unconditional rule where it is harmless; skip it for elevated roads the lane geometry already paves, and keep it for elevated roads it does not reach.
- Let the person who can see the defect name it. Colour-coding layers answers "which layer is this?" and a height ramp answers "is it at the right level?", but both still need someone to interpret a colour — and if you are sampling the wrong pixel, both will tell you everything is fine. A click-to-identify raycast that reports layer, height and depth order under the cursor closes that gap: whoever can see the problem points at it, and gets back a named layer at a measured height.
- Keep provenance through a batch. Merging thousands of road polygons into one buffer is the right call for performance and it discards exactly the fact you need when a surface is in the wrong place: which feature produced it. Keep one record per TRIANGLE alongside the position buffer — a few hundred references — and a raycast's face index turns any pixel back into a named way with its class, its kind and its layer tag.
- A
layertag describes a whole way, and a ramp is not at one level. OSM tags an entire slip road with the layer of the structure it serves, so a ramp climbing from a roundabout to a second-level deck reads aslayer=2end to end. Seed every one of its nodes at full height and there is nothing left to relax — the ramp renders flat two levels up and stops in mid-air over the ground it should meet. Pin the node where an elevated way shares a node with an at-grade way to the ground: that is a touchdown, and because ways that merely CROSS share no node, nothing is dragged down by a road passing underneath. Let the gradient limit spread the descent from there. - Sweep a deck, do not tile a model along it. An imported deck module is a rigid object standing in for a shape that differs at every junction: it needs pitching by hand, per-axis scaling, an overlap to hide its joints, a distance gate when its detail breaks up, and a way to stop it fighting whatever else paves the road. Sweeping the centreline with a width and a thickness gives a running surface, a soffit, two fascias and closed ends that follow the profile exactly, with no joints and nothing that can be oriented wrongly. Check the asset before trusting it — ours had a full-width edge only ~0.5 m deep, one pixel at the wider framings, so it never had a readable body to begin with.
- Give every face its own vertices. Sharing four vertices per station between the running surface, the soffit and both fascias makes
computeVertexNormalsaverage across the 90° arrises, and the slab shades as a rounded tube instead of a solid with edges. Duplicate per face; a deck is rebuilt once per junction. - Never lay a decoration on a deck with a ground-draping helper. If deck heights already include the terrain sample and the helper adds it again per vertex, kerbs and edge lines climb off the structure by the full terrain height. Build anything attached to a deck into the deck's own geometry.
- A pier carries the SOFFIT. Sizing a column to the road height leaves it a slab-thickness short of what it holds up, and the daylight between them is exactly what makes a viaduct read as floating. Skip piers where clearance is less than a column is tall — near a touchdown they become stubs jammed under a ramp that is about to land.
- Seeding every point of a structure fixes it in place. If each node of a bridge is a zero-distance seed, the relaxation that brings height down to grade never runs ALONG the structure — only outward onto its neighbours. A deck vertex 29 m from its own landing stays at full height, and a 0.34 gradient limit can shed under 10 m over that distance, so the carriageway ends metres in the air behind an end wall. Add a second pass that caps height by each node's distance from grade THROUGH the graph, at a gradient just under the limiter's so the two never fight.
- Solve a structure's profile once and share it. If the deck is built from welded ways while the painted surface resolves height per individual way, the two disagree — the limiter pulls a chain's high section down toward its landing, but a lone way whose nodes are all fixed at full height has nothing to pull against. Measured divergence: 10 m, i.e. 40 m of road hanging over open air above its own deck. Solve per structure, then prime each member's cache from it.
- Resample before you sweep. Vertex spacing in the source data has nothing to do with a bridge's shape: a 397 m viaduct arrived as five vertices, so its entire climb was one quad with nothing between 99 m and 207 m. Resample linearly along the polyline — not through a smoothing spline, which interpolates height in 3D and overshoots above a flat deck section.
- Copy a memoised array before rewriting it. Densifying in place mutates the cache every other consumer reads, and hands them a profile indexed against vertices that no longer exist.
- Blend the camera's CLEARANCE, not its altitude. Interpolating between a high travelling pose and a road-level one makes the eye plummet toward the surface before the road starts to descend — measured 12 m in a third of a second, arriving under 2 m off the tarmac while the road was still at grade. Hold a clearance above the road and let the road carry the eye down: the descent then belongs to the road, which is what the driver is meant to be reading.
- Smoothing a mask is not the same as easing it. Smootherstep's whole shape is a steep middle; applied to a mask that is already a moving average it CONCENTRATES a transition instead of spreading it. Widen the mask to spread a change; leave its shape alone.
- Trigger a constraint on the condition it protects, not on proximity to it. A rule keeping the eye under a tunnel roof, gated on nearness to the tunnel, fires while the road is still at grade — where the computed roof sits below the road itself — and drives the camera into the floor. Gate it on the road actually being below ground: the same test the geometry uses to seal the tube.
- The
layertag orders structures that OVERLAP, and says nothing about ones that merely cross. Two flyovers crossing each other are routinely bothlayer=1— that means "both are bridges", not "both at the same height", and drawing each one level up passes two decks through one another. Solve the level instead: impose an order wherever two elevated ways cross in plan without sharing a node, and take the longest path through it (capped, or a diagram becomes a stack). Let the route's own structure win ties — the road being driven should pass over the one it crosses, because that is the reading that answers "where am I going". - A 2D lane surface must stop at the lip of an excavation. Lane geometry takes its height from the way it came from; a trench takes its height from the route profile. Near a portal those are the same road reaching the same place by two different paths, and they disagree — measured six metres, which paves a slab of tarmac across the entrance and hides the portal behind it. Suppress it inside the open cutting (the shell's invert and the driven carriageway already draw the road there, both from the profile the camera flies) and let it continue over a covered bore, where the ground really is intact.
- Suppress per TRIANGLE, not per polygon. Lane surface arrives as a handful of large shapes, one of which is the whole junction, so a centroid test lets a slab whose middle is well clear of the trench pave straight across it.
- Size a "distance to grade" search radius for the TALLEST structure present, not for one level. A radius of
oneLevel / gradientcan only ever yield a cap of one level's height, so the moment the out-of-range case stops meaning "uncapped" it silently pins every second-level deck to a first-level height — a regression no gradient check can see, because the result is still a legal profile. - A tunnel portal is NOT a touchdown. Where a bridge meets the surrounding network it lands, so pinning the shared node to grade is the whole point. A bore does the opposite — it is already at full depth there, because the road descended into a cutting to reach it and that cutting is tagged
layer=0like any other road. Pin it anyway and the depth has nowhere to go but INSIDE the tunnel: a 54 m bore with five nodes and both ends pinned solved to0, −2.6, −7.7, −4.6, 0, a bathtub at the slope limit. Seed a bore at full depth along its whole length, mouths included, and relax the climb outward along the approach ways. - Derive the ramp length from the depth, not from a scene constant. One global relaxation radius is only correct while every structure is the same height; a layer −2 bore given the same run as a layer −1 one arrives at the surface doing twice the gradient, which the limiter then shears into a step. Hold the GRADIENT constant across the diagram — it is the property a driver reads — and remember a smoothstep's steepest point is 1.5× its average, so size for the peak.
- Sample the route's height ALONG the road, not at its nodes. A routing engine emits its own polyline and only reproduces a way's node positions where the geometry demands it, so a grid lookup keyed on coincidence returns zero over most of the approach — a flat profile then a cliff. Project onto the nearest driven way and interpolate its solved vertices; it is continuous by construction and does not depend on two independent geometries agreeing.
- One scalar, not thirteen depth constants.
cover = groundOffset − routeZ(metres of earth over the road) is enough to decide where a roof exists, how tall the room is, where the portals are and what the camera may do. Thirteen separately-defensible thresholds described no single shape: the mouth did not fit the tube, the roof was drawn below the floor, and the open cutting was re-lidded 0.25 m lower along 100% of its length. - Make the portal the JOIN, not an object. An opening built by its own arithmetic can be provably impossible: bound the arch radius above by one constant and the tube half-width below by another and
mouth < tubeholds unconditionally — measured a 2.4 m opening onto an 11.6 m bore, overhung by 4.4 m of carriageway each side. Build the mouth as the annulus between the trench section and the bore section taken at the same station, and a mouth cannot mismatch a tube it is a slice of. - Size the roof so the camera fits, then nothing else can go wrong. Choose the clear height by bisection subject to
H ≥ eyeHeight + eyeClearance + band, and roof only wherecover ≥ H + slab + freeboard. Two theorems fall out: nothing can erupt through the surface, and the camera's floor and ceiling terms can never cross — which was the single defect behind every broken frame. - A clamp that a floor can override is not a clamp.
max(min(z, roof − c), road + k)lets the floor win wherever the roof is low, i.e. at every portal on Earth. Clamp AFTER smoothing, at the eye's OWN station — the eye trails the vehicle by tens of metres, and solving its height at the vehicle's station buries it under the approach carriageway on any gradient. - A footbridge is not a viaduct.
bridge=yesdescribes construction, not who may drive on it. Building pedestrian crossings as carriageway decks puts a phantom road viaduct over the junction and pollutes an elevation solver that works over shared nodes. Grade separation in a driving diagram means separation of things that can be driven. - Cut the excavation by deleting grid triangles, not by pushing an offset ring into a triangulator. An offset polyline has no mitre limit and no self-intersection repair, and earcut does not throw on a bad ring — it emits garbage, so a
try/catcharound it can never fire. Delete a triangle only when all three corners are inside the trench: the residual is always an overhang of one cell, never a gap onto sky. - Verify the winding, do not reason about it. Take the first non-degenerate triangle, dot its normal against the direction of the enclosed air, and reverse the whole index buffer once if it points the wrong way. With single-sided materials that one test gives all three behaviours at once: visible from inside the bore, see-through from outside an open cutting, and invisible from above a covered one.
- Blend CLEARANCE, not altitude, when a camera follows a descending road. Interpolating between a high chase pose and a road-level one makes the eye plunge toward the tarmac while the road is still level, and only afterwards follow it down — measured 11.8 m in a third of a second, arriving 1.7 m off the surface. Hold a clearance above whatever the road is doing and let the road carry the camera down; the descent then reads as the road's, which is what it is.
- Do not smooth a ramp with a curve whose shape is a steep middle. Smootherstep applied on top of an already-smooth mask concentrates the transition instead of spreading it. Widen the mask; leave its shape alone.
- Gate a "duck under the roof" clamp on the road actually being below ground, not on proximity to a tunnel. Widening the approach for a gentle descent makes any proximity mask read true long before the portal — and the roof computed there sits below the road, so the clamp slams the eye to its floor and reintroduces the exact step the smoothing was meant to remove.
- Audit junctions you have never seen. A fixed regression set proves known junctions still render; it says nothing about the interchange nobody has looked at. Assert instead the properties a correct junction has anywhere: nothing non-finite, no road climbing faster than the ramp scale, elevation span physically plausible, tarmac under the route, lane geometry reaching the manoeuvre, and the camera never below ground unless genuinely inside a covered bore. Discover real structures from the API rather than hand-picking coordinates, so the sweep does not get to choose easy cases.
- Street furniture is not the subject. Poles placed along the route have to be scaled up to be visible at junction framing, at which point they become a row of oversized posts marching down the shot. A junction view is a diagram of a decision.
- Dilate a mask before you smooth it. Smoothing a "inside the bore" mask directly eats the transition into the tunnel from both ends: the factor was still 0.78 a third of the way through, which put the camera 3.4 m up — through the roof and out into open sky mid-tunnel. Dilate first so the ramp sits outside the portal, then smooth, and the factor stays 1.0 for every metre of actual bore.
- Stop the route at the portal and resume at the far one. Drawing it through the bore as a translucent x-ray keeps the thread but makes a tunnel read as a stretch of dimmer road. A clean gap reads as something the road goes INTO. Decide "inside the bore" from the bore's own centreline — proximity AND parallelism, because a loop ramp swings back past its own tunnel and a plain distance test called 138 of 170 route points "in the tunnel" for a 52 m bore.
- Resample the route before you measure anything along it. A routing engine's shape can be 9 vertices over 559 m. That places a ribbon but cannot say WHERE along it anything happens — the gap could only ever start at whichever vertex was nearest the portal, up to 60 m out. Resample to ~6 m. Solve elevation and lane offset on the ORIGINAL vertices first and interpolate them onto the resampled line: both match route points to way NODES by coordinate coincidence, and a resampled point matches nothing, so the ribbon otherwise alternates between the driving line and the centreline every 6 m and comes out visibly serrated.
- Route through the WHOLE structure, not the way you were handed. A bore or viaduct is routinely several ways. Offsetting the endpoints of one of them puts the route's start inside the next way of the same structure, so the returned shape never leaves the tunnel — and a client that stops drawing through the bore then has nothing left to draw. Chain endpoint-to-endpoint first, then offset from the real portals.
- One set of ways, everywhere. The structure chain, the route window and the renderer must agree. Chaining over ways found around the search seed while the renderer used ways re-fetched around the structure gave the renderer a longer bore than the window was sized for — with the same empty result.
- Stand at the start of the route, but bound the distance. Anchoring the eye on the route's first vertex makes the view begin where the journey through the junction begins. Take the DIRECTION from the route's heading at that point, not the vector from the aim point to it — on a straight tunnel that baseline can be 56 m, where a few metres of geometry swing the whole camera. Cap the distance, and aim at the junction itself: aiming short of it pushes it toward the horizon, where a low camera compresses it to nothing.
- A fixed-distance route window truncates anything that loops. "160 m back, 150 m forward" suits a straight manoeuvre. The ramp under Airport Road turns through 895°; a 310 m window captured 585 of them, so the route was drawn entering the loop and then simply stopping mid-turn. End the window where the MANOEUVRE ends instead: keep extending while the path is still turning, and stop once it has run straight for ~80 m, with a cap around 420 m each way. Straight manoeuvres are unaffected — they stop at the minimum — while the loop opens to ~410 m each way and captures 889 of its 895°. Because a loop returns on itself, that is still only 232 × 242 m of ground, so the frame does not have to open up.
- Bound the walk BEFORE committing a step. Route shapes are sparse on fast roads — a single vertex can jump 200 m — so testing the cap after adding overshoots it by half the window again.
- Look at the junction obliquely, not straight down the approach. The driver's own view is the worst angle for reading a junction: a roundabout collapses to an ellipse, a loop hides its return leg behind its outbound leg, and a bore passing underneath is edge-on. Swinging ~35° off the approach heading and lifting the camera ~10° turns a foreshortened smudge into a route visibly entering one portal, crossing beneath the roundabout, and leaving by the other — the same manoeuvre, read as a shape.
- Tile a deck module, don't stretch it. A bridge deck curves and climbs. Stretching one model between the abutments replaces that with a straight chord; placing one instance per ~20 m along the centreline, each on the local tangent and the local solved height, follows the real deck. Walk by ARC LENGTH rather than per vertex — OSM vertex spacing is uneven, so per-vertex tiling gives 3 m modules on a ramp and 60 m ones on a motorway. Divide the total into equal steps so there is no stub at the far end, and overlap each tile a few percent so joints cannot open a gap.
- Ship one module, not the kit. A modular kit holds every variant side by side; the view usually tiles one of them. Extracting just that module — with only the accessors, materials and textures it references — took our bridge kit from 4.67 MB / 36.6k triangles to 1.00 MB / 7.1k. Normalise the extracted module to a predictable frame (length along X and centred, width along Z and centred, road surface at
y = 0) so placement is per-axis scaling with no per-asset fudge factors. - Scale a deck per axis. Width to the carriageway, length to the tile's own run, but hold thickness roughly constant — carrying the width factor (often ~3.5x) into depth turns a 2.5-unit deck into a 9 m slab.
- The route ribbon must be NARROWER than the road it sits on. Sizing it purely in pixels ignores what is underneath: at one framing a 19 px target came out at 11.8 m over an 8.5 m trunk road, so the ribbon spilled over both kerbs and read as a blue road of its own. Keep the pixel width as a ceiling for legibility, then clamp it to the carriageway — we use ~0.56x for the fill and ~0.86x for the casing. Derive the carriageway width from the lane count where it is tagged, not from the road class alone; a 4-lane primary is far wider than its class implies.
- Find the real bore ends first. Mappers split tunnels into several ways (at layer changes, at lane splits, anywhere they stopped), so a way's own endpoints are usually mid-bore. The true ends are the nodes used by exactly one tunnel way; every other shared node is an internal joint. Placing mouths at way endpoints buries them inside the tunnel.
- One stretched instance, not two mouths. A run that is open at both ends gives the entrance and the exit from a single object, correctly joined in between. Two separate mouths leave the middle hollow and overlap each other on a short bore.
- Fit per axis, not uniformly. Scale width to the cutting the route runs in and height to the cutting's depth. A uniform scale sized to width pushes a 12-unit shell well above the street, and the roof then reads as a raised asphalt strip — the opposite of a tunnel.
- Finish the crown BELOW the ground plane. Fitting the height to the trench depth alone lands the roof at exactly
z = 0, which is above a ground plane that sits slightly lower. Sink it under the surface; that is what makes it read as running beneath the street. - Keep the procedural headwalls. Buried under the street the shell is invisible from above, so the arched portal face at each end carries the whole entrance/exit read. The model supplies the body, the headwall supplies the mouth.
- The mouth needs an UNLIT rim. A lit headwall is near-black against near-black ground in a dark theme and the entrance simply disappears. An unlit band tracing the arch holds its brightness at any camera angle and in either theme. Extrude it slightly deeper than the headwall so its face sits in front, instead of z-fighting as a coplanar decal.
- Budget the textures. Junction assets load lazily during navigation. A single 4K diffuse map can be 10 MB on its own; at the size a tunnel mouth occupies on screen that is thousands of texels per pixel. Navvo caps junction textures at 1K, which took its tunnel asset from 15.1 MB to 0.95 MB with no visible difference.
- Rebuilding a GLB means rewriting every offset. Buffer views are packed sequentially into one buffer, so resizing any texture shifts everything after it. Re-align each view to 4 bytes, update the buffer length, and rewrite the total length in the header — the JSON chunk pads with spaces (
0x20), the binary chunk with zeros.
Successful responses are cached server-side per ~120 m cell for 24 h (bridge/tunnel geometry is static).
GET /junction/streets
Lane-level surface geometry for a junction: the road and intersection polygons plus every painted marking, generated server-side from Navvo's own structures data.
Scope required: geo
Where /junction/structures tells you what is there (a bridge, a tunnel, the surrounding network), this tells you what it looks like on the ground — the actual asphalt shapes and the paint on them, ready to draw.
curl -s "https://navvo.io/api/v1/junction/streets?lat=31.9539&lng=35.9106&radius=160" \
-H "x-api-key: nvvo_YOUR_KEY"
Query parameters
300.Response
{
"source": "osm2streets",
"available": true,
"roads": { "type": "FeatureCollection", "country_code": "JO", "features": [ /* road & intersection polygons */ ] },
"markings": { "type": "FeatureCollection", "features": [ /* lane separators, centre lines, turn arrows */ ] }
}
false when no lane-level geometry could be generated for that point. Fall back to /junction/structures or a plain route line — do not treat it as an error.road or intersection. Draw intersections as a single paved disc rather than overlapping road ribbons, or every junction reads as a pile of stacked bands.radius. Roughly 1.44 MB at 320 m, 701 KB at 220 m and 390 KB at 160 m. Ask for the smallest radius that fills your frame; the default 300 is generous for a full junction view and wasteful for a thumbnail.GET /junction/route-through
The real driven line through a bridge or tunnel at a point — the shape a vehicle actually follows, rather than the structure's outline.
Scope required: geo
curl -s "https://navvo.io/api/v1/junction/route-through?lat=31.9539&lng=35.9106&kind=tunnel" \
-H "x-api-key: nvvo_YOUR_KEY"
Query parameters
::::field{name="lat" / name="lng" type="number" required} The point on the structure. ::::
tunnel (default) or bridge. Anything other than bridge is treated as tunnel.Response
{
"source": "navvo-junction",
"available": true,
"shape": [[35.913101, 31.95377], [35.912649, 31.953971], "…"]
}
shape is an ordered [lng, lat] polyline. Draw it semi-transparently through the bore rather than clipping it at the portal — a route that vanishes at a tunnel mouth reads as a routing failure to the driver.
Building a junction view into a live navigation product
Everything above describes the data. This section is about running it in a car, and it is written from Navvo's own turn-by-turn client, which drives this endpoint on every manoeuvre.
Open the window on the manoeuvre, not on the map
A junction view answers one question — where do I go at the next decision point — so its lifetime
is the approach to one manoeuvre. Navvo opens it at 220 m for a city turn and 420 m for a
motorway exit, merge, fork or anything grade-separated, and closes it 6 m short of the manoeuvre.
Latch the state with hysteresis (on inside the range, off only beyond range + 60 m) or the panel
flickers at the boundary — and clear that latch on every route change, or the first manoeuvre of
a new route inherits the old one's latched true and pops its window early.
Prefetch the structures, lazily fetch the lanes
GET /junction/structures is small — centrelines, not geometry — so fetch it for the next two or
three steps ahead regardless of whether their windows will open: whether a bridge or tunnel sits at
the manoeuvre is itself part of deciding when to open. GET /junction/streets is the expensive
one and should be fetched only when a window is actually about to open.
Use the same radius for the prefetch and the render fetch. The cache buckets by radius, so asking for 450 m ahead of time and 400 m at the moment of use is two keys and two upstream queries for the same junction.
Sizes, measured
| call | radius | payload |
|---|---|---|
/junction/structures | 450 m | ~10–30 KB compressed (the upper end in a dense city block) |
/junction/streets | 180 m | ~800 KB raw |
/junction/streets | 220 m | ~700 KB raw / ~78 KB compressed |
/junction/streets | 360 m | ~2.0 MB raw |
Lane geometry scales superlinearly, and past a couple of hundred metres it is drawn under road
surfaces you already have from structures.roads rather than instead of them. Do not derive the
lane radius from the length of your route window. Pin it — 220 m is indistinguishable from 360 m
in a junction-wide frame and costs a third as much.
Degrade to a diagram, never to a spinner
| Situation | What you get | What to draw |
|---|---|---|
| Ordinary at-grade junction | bridges: [], tunnels: [], roads and buildings present | The normal view. This is the common case, not a failure. |
| Lane geometry slow or unavailable | streets missing or available: false | Fall back to carriageway ribbons from the structure centrelines. Visibly simpler at lane scale, entirely legible. |
| Outside coverage | available: true with every array empty | Do not open the window. |
| Index unreachable / offline | available: false | Do not open the window. |
Note the third row. available means the geometry index answered, not there is data here — a
junction outside coverage answers successfully with nothing in it. Gate on
roads.length > 0, or you will fade in a fully-lit empty box. Put a hard deadline on the lane
request too (Navvo uses 4.5 s): a payload that lands after the driver has passed the junction has
cost data and battery for nothing.
The panel is a 3D scene, and it must know when nobody is looking
Keep it mounted for the whole session — creating it per manoeuvre costs more than idling it — but
give it an explicit active flag from the navigation state. An IntersectionObserver reports a
transparent element as perfectly visible, so a panel faded to opacity: 0 will otherwise render at
full frame rate, on a phone, on battery, for the 95 % of a drive that is not an approach.
Three more things that only show up in a car:
- Size the panel by RATIO, not by pixel height. Metres-per-pixel depends on panel height, so one fixed height gives a phone and a desktop different framings of the same junction — and a panel tall enough to look right on a large phone will cover the vehicle marker on a small one.
- Separate the scene's identity from the distance label. The distance counts down every few metres; if it shares an object with the scene payload, every tick rebuilds the geometry and restarts the camera, and the driver sees the first half-second of the approach on a loop.
- Include the step index in that identity. A route that loops or doubles back passes the same manoeuvre coordinate twice with a different approach each time.
Handle a lost WebGL context
A navigation page can hold several WebGL contexts, and a mobile browser that runs out evicts the
oldest — which may be your main map. Listen for webglcontextlost on the junction canvas and stop
opening the window for the rest of the session. A closed panel is correct; a black rectangle over
the driver's route is not.