Debugging Clipped Geometry at Tile Boundaries
Coordinates outside a tile’s 0..extent grid are not corruption. They are the buffer: a strip of each neighbour’s geometry carried into the tile so that a road casing or a polygon outline drawn near the edge meets its continuation cleanly. Most reported “clipping bugs” are one of four different problems, and telling them apart takes one decode and one screenshot.
When to Use This
A hairline gap along tile boundaries at some zoom levels. A polygon outline that stops abruptly mid-edge. Labels appearing twice near a seam. Or a decoder that rejects a tile because it validates coordinates against 0..4096.
Specification Detail: The Buffer
Every generator clips features to a box slightly larger than the tile, because a tile must be renderable on its own and a renderer never sees its neighbours while decoding one.
| Setting | Where | Default | Units |
|---|---|---|---|
--buffer |
Tippecanoe | 5 |
Units of a 256-unit reference tile |
| Effective buffer | Derived | 80 |
Units of the 4096 extent |
extent |
Layer field | 4096 |
Tile-local grid units |
So a default build produces coordinates in roughly -80..4176, and geometry in that margin is drawn and then masked by the renderer. That masking is why the buffer is invisible when everything is configured correctly, and why doubling it inflates every tile for something no reader ever sees.
Production Command: Reading the Real Coordinate Range
Decode one tile and look at the extremes, rather than inferring from the rendered result:
import gzip
import mapbox_vector_tile
raw = open("14_8188_5449.mvt", "rb").read()
if raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
tile = mapbox_vector_tile.decode(raw)
for name, layer in tile.items():
xs, ys = [], []
for feature in layer["features"]:
def walk(node):
if isinstance(node, (list, tuple)) and node and isinstance(node[0], (int, float)):
xs.append(node[0]); ys.append(node[1])
elif isinstance(node, (list, tuple)):
for child in node:
walk(child)
walk(feature["geometry"]["coordinates"])
print(f"{name:14s} extent={layer['extent']:5d} "
f"x=[{min(xs)}, {max(xs)}] y=[{min(ys)}, {max(ys)}]")
# roads extent= 4096 x=[-80, 4176] y=[-80, 4176] <- healthy buffer
# buildings extent= 4096 x=[0, 4096] y=[0, 4096] <- no buffer at all
The second line is the diagnostic. A layer whose coordinates stop exactly at 0 and 4096 was built with --buffer 0, and it will show seams wherever anything is stroked.
Four Problems That All Look Like Clipping
A buffer that is too small. Symptom: a hairline gap along tile edges, appearing at some zooms and not others. Cause: stroke width in screen pixels is constant while the tile’s ground extent halves each zoom, so the buffer measured in tile units covers fewer screen pixels as you zoom out. Fix: raise --buffer for the affected layer only, and check the range at the widest zoom where the layer is drawn.
Geometry genuinely truncated. Symptom: a polygon’s outline ends mid-edge with no continuation in the neighbouring tile. Cause: the feature was dropped from the neighbouring tile by a dropping strategy, not clipped. Fix: check the neighbour tile for the feature id before touching the buffer.
A winding problem misread as clipping. Symptom: a polygon appears inverted near an edge — the hole filled and the body hollow. Cause: ring winding, not clipping; the clip operation re-orders rings and a decoder that assumes the first ring is always exterior gets it wrong.
Stroke width, not geometry. Symptom: seams that move when the map is resized. Cause: a line-width expression producing sub-pixel widths at some zooms, where rounding differs between adjacent tiles. Fix is in the style, and no buffer setting will help.
Interaction Effects
With label deduplication. A point label near a boundary appears in both tiles, and the renderer deduplicates it by feature id. A missing or non-unique id therefore produces doubled labels only at tile boundaries — a symptom that looks like a clipping artefact and is an identifier problem. Preserve ids with --generate-ids or a stable source id, and see attribute filtering for why the id is worth its bytes.
With tile size. The buffer strip is duplicated into all four neighbours, so raising --buffer from 5 to 20 can add 10–15% to a dense tile for geometry that is masked and never seen. Raise it per layer, never globally, and re-measure the worst tile afterwards.
With overzoom. Overzoomed tiles reuse the deepest generated tile, so the buffer is scaled up along with everything else and seams do not reappear. This is one of the few places overzoom is strictly better than generating another level.
Performance Impact
Buffer size is a direct multiplier on the duplicated band. Measured on a dense z14 road tile, --buffer 5 adds about 4% to the payload over --buffer 0; --buffer 20 adds about 14%; --buffer 64 adds close to 40%. None of that geometry is ever visible.
The right approach is therefore to treat the default as correct until a seam is actually observed, then raise it only for the layer that shows the seam, and only to the smallest value that closes it.
Common Mistakes
Validating coordinates against 0..extent in a custom decoder. Discards the buffer and reintroduces the exact seams the buffer exists to prevent.
Raising the buffer globally to fix one layer’s casing. Inflates every tile in the tileset for one layer’s problem.
Comparing tiles at different zooms. A seam visible at z12 and absent at z14 is normal and is a stroke-width relationship, not evidence that the z14 build is correct and the z12 build is broken.
Assuming a gap means missing data. Decode the neighbouring tile and look for the feature before changing any build flag. Half the time it is there and the problem is entirely in the style.
FAQ
What buffer value should I start from?
The Tippecanoe default of 5 — that is, 80 units on the 4096 extent — is correct for the great majority of tilesets and should be left alone until a seam is actually observed. When one is, raise it for the affected layer only, in small steps, and re-measure the worst tile after each change.
Does the buffer affect points?
Only for label placement. A point in the buffer strip is drawn and masked like any other geometry, but its label may extend into the visible area, which is exactly why the buffer exists for point layers at all. Deduplication by feature id then prevents the label appearing twice.
Why does a seam appear at one zoom and not another?
Because stroke width is constant in screen pixels while the buffer is constant in tile units, and a tile covers half as much ground at each successive zoom. The same 80-unit buffer therefore covers a different number of screen pixels at every zoom, and the seam appears wherever that number falls below the stroke’s half-width.
Can I set the buffer to zero to save bytes?
Only for layers that are never stroked and never labelled — a fill with no outline, for instance. Anything with a line-width, a halo or a casing will show hairline gaps at tile boundaries, and the saving is a few percent at most.
Related
- MVT Encoding Internals — the parent topic, and where the
extentfield lives. - MVT Geometry Command Encoding Explained — reading the raw coordinates this page inspects.
- Controlling Tile Size with Drop and Coalesce Flags — the flags that make a feature vanish from one tile and not its neighbour.
- Geometry Simplification Algorithms — the shared-node problem, which produces gaps that look identical and are not.