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.

What a tile carries beyond its own edgesA tile grid with the centre tile highlighted along with the strip of surrounding geometry it carries, showing that each neighbour's edge geometry is duplicated into the tile.THE BUFFERtile2/2/12/1/2x → easty → southThe tile being decoded: coordinates0..4096Neighbour geometry carried in thebuffer: roughly -80..0 and 4096..4176The renderer draws the buffer stripand then masks it at the tile edge
Each tile carries a strip of all four neighbours. That duplication is the cost of the buffer, and it is why the setting is a trade rather than a free win.

Production Command: Reading the Real Coordinate Range

Decode one tile and look at the extremes, rather than inferring from the rendered result:

python
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.

Four seam symptoms and what each one actually isA decision tree separating a too-small buffer, a dropped feature in the neighbouring tile, a ring winding error and a sub-pixel stroke width problem.DIAGNOSEWhat exactly does the seam look like?Hairline gap, onlyat some zoomsBuffer too small for thestroke width at that zoomOutline stops, nocontinuation nextdoorThe feature was droppedfrom the neighbour, notclippedFill and holeappear swapped nearthe edgeRing winding after theclip, not the clip itselfSeam moves when thewindow is resizedSub-pixel line-widthrounding — a style problem
What each buffer setting adds to a tile that never rendersPayload increase over a zero buffer for Tippecanoe buffer values of 5, 10, 20 and 64 on a dense z14 road tile.BUFFER COST% added to a dense z14 tile--buffer 5 (default)+4%--buffer 10+8%--buffer 20+14%--buffer 64+39%
The buffer strip is duplicated into all four neighbours and then masked. Raising it globally to fix one layer's casing is a tileset-wide cost for one layer's problem.

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.