Choosing Tile Extent and Buffer Values Jump to heading

Replace the two numbers everybody copies from an example with two numbers derived from your own style sheet and zoom range — and know what each one costs per tile.

Prerequisites Jump to heading

Conceptual minimum Jump to heading

Extent sets the resolution of the tile’s internal grid. A tile is conventionally rendered at 512 screen pixels; with an extent of 4096 that is eight grid units per pixel, which is comfortably finer than any display can show. Raising the extent adds precision nobody can see while making every coordinate delta larger and therefore more expensive to encode. Lowering it saves bytes at the cost of visible quantisation.

Buffer sets how far beyond the tile edge geometry is retained. Its purpose is entirely about rendering: a line styled eight pixels wide extends four pixels either side of its centreline, so if the centreline is clipped exactly at the tile edge, the outer four pixels of the neighbouring tile have nothing to draw. The result is a hairline seam that appears and disappears as you pan.

The conversion between the two units is the whole calculation. With an extent \(E\) and a tile rendered at \(P\) pixels, one screen pixel is \(E/P\) grid units. A style whose widest stroke is \(w\) pixels therefore needs at least

bline=w2EPb_{\text{line}} = \frac{w}{2} \cdot \frac{E}{P}

grid units of buffer. Labels need more, because a label anchored near the edge extends much further than a line: the half-width of the largest label box replaces \(w/2\).

What the buffer has to cover, measured outward from the tile edge A band running outward from the tile edge divided into four zones by what must be retained. The first zone covers half the widest styled line, which is the minimum any base map needs. The second covers icon and symbol half-widths, which are typically larger than line widths. The third covers label text boxes, which extend furthest because a label anchored near the edge runs well past its anchor. The fourth is spare margin for client-side effects such as halos and blur. A note adds that the buffer is expressed in grid units, so the pixel figures must be converted. Four things live in the buffer, and labels dominate line half-width widest stroke / 2 the minimum typically 4 to 8 px cheapest to cover icon half-width largest symbol / 2 usually bigger than lines sprites are 16 to 32 px cheap enough label box longest label / 2 dominates the requirement can exceed 100 px the expensive term effects halos and blur a few pixels easy to forget add a small margin Sizing the buffer from line widths alone is why labels near tile edges disappear on some pans and reappear on others.
Labels are the term that decides the buffer, and they are also the one most often left out of the calculation.

Runnable solution Jump to heading

python
from __future__ import annotations

import logging
from dataclasses import dataclass

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger("osm.mvt.buffer")

TILE_PIXELS = 512          # the size one tile is rendered at


@dataclass(frozen=True)
class StyleFacts:
    widest_line_px: float          # the thickest stroke in the style
    largest_icon_px: float         # the biggest sprite, edge to edge
    longest_label_px: float        # the widest label box you expect to render
    effect_margin_px: float = 4.0  # halos, blur, outline


def buffer_units(style: StyleFacts, extent: int = 4096,
                 tile_pixels: int = TILE_PIXELS) -> int:
    """Grid units of buffer needed so nothing is clipped mid-symbol."""
    units_per_pixel = extent / tile_pixels
    needed_px = max(
        style.widest_line_px / 2.0,
        style.largest_icon_px / 2.0,
        style.longest_label_px / 2.0,
    ) + style.effect_margin_px
    units = int(round(needed_px * units_per_pixel))
    logger.info("%.1f px of overhang -> %d grid unit(s) at extent %d",
                needed_px, units, extent)
    return units


def extent_for_precision(min_zoom: int, max_zoom: int,
                         target_metres: float) -> int:
    """Smallest conventional extent giving <= target_metres per unit at max_zoom."""
    circumference = 40_075_016.686
    for extent in (256, 512, 1024, 2048, 4096, 8192):
        unit_m = circumference / (2 ** max_zoom) / extent
        if unit_m <= target_metres:
            logger.info("extent %d gives %.2f m/unit at zoom %d (%.0f m at zoom %d)",
                        extent, unit_m, max_zoom,
                        circumference / (2 ** min_zoom) / extent, min_zoom)
            return extent
    logger.warning("no conventional extent reaches %.2f m at zoom %d",
                   target_metres, max_zoom)
    return 8192


def estimated_overhead(buffer: int, extent: int) -> float:
    """Fraction of tile AREA that is buffer, as a first-order cost proxy."""
    inner = extent
    outer = extent + 2 * buffer
    overhead = (outer ** 2 - inner ** 2) / inner ** 2
    logger.info("buffer %d at extent %d covers %.1f%% extra area",
                buffer, extent, overhead * 100)
    return overhead


if __name__ == "__main__":
    style = StyleFacts(widest_line_px=10, largest_icon_px=24,
                       longest_label_px=180)
    extent = extent_for_precision(min_zoom=0, max_zoom=14, target_metres=1.0)
    buf = buffer_units(style, extent=extent)
    estimated_overhead(buf, extent)

Step-by-step walkthrough Jump to heading

  1. Take the maximum, not the sum. Only the largest symbol needs to fit; a tile does not need a buffer wide enough for a line and an icon and a label stacked together.
  2. Include labels in the maximum. A 180-pixel label box needs a 90-pixel overhang, which at extent 4096 and 512-pixel tiles is 720 grid units — many times what a line width alone would suggest.
  3. Add an effect margin. Halos, outlines and blur extend a symbol past its nominal box, and a few pixels of slack costs almost nothing.
  4. Derive the extent from precision, not from habit. extent_for_precision walks conventional values and returns the smallest that meets the ground resolution you actually need at the maximum zoom.
  5. Check the low-zoom end too. The same function reports metres per unit at the minimum zoom, which is the figure that explains blocky low-zoom coastlines to whoever asks about them.
  6. Measure the cost as area. The overhead calculation is a first-order proxy: a buffer of \(b\) around an extent \(E\) retains geometry over an area larger by \(((E+2b)^2 - E^2)/E^2\). At extent 4096 and buffer 64 that is about 6 percent; at buffer 720 it is over 90 percent.
  7. Reconcile the two numbers. A label-driven buffer this large is a signal to reconsider: most production pipelines put labels in a separate layer with a larger buffer, or accept that labels near edges are placed by the client from point anchors rather than from clipped geometry.
Extra geometry area retained at several buffer values, at extent 4096 Five buffer values with the proportion of additional area each retains beyond the tile itself. A buffer of sixteen grid units retains about one and a half percent extra. Sixty-four units retains about six percent. One hundred and twenty-eight units retains about thirteen percent. Two hundred and fifty-six units retains about twenty-seven percent. Seven hundred and twenty units, which is what a large label box demands, retains over ninety percent — nearly doubling the geometry in every tile. What each buffer value costs in retained geometry buffer 16 about 1.6% more buffer 64 about 6% more buffer 128 about 13% more buffer 256 about 27% more buffer 720 about 92% more The last value is what a 180-pixel label box demands, which is why label anchors usually live in their own thin layer.
Cost grows roughly linearly in the buffer at small values and painfully faster once the buffer approaches the extent.
What to do when the buffer a style demands is too expensive A decision node about a buffer requirement driven by large labels, with three outcomes. Splitting labels into their own thin point layer lets that layer carry a generous buffer while the geometry layers keep a small one, which is the usual production answer. Reducing the largest label size in the style lowers the requirement directly and is worth checking with the designer. Accepting occasional clipped labels is defensible for a thematic map where labels are sparse and a missing one near an edge is not a serious defect. The buffer the labels want is too expensive — now what? Whose requirement can move? Three options, all legitimate Pick before generating tiles Split the label layer Thin point layer carries the big buffer; geometry keeps a small one Reduce the label size A style change lowers the requirement at its source Accept some clipping Sparse labels on a thematic map; a rare miss is tolerable The first branch is the usual production answer because a point layer with a large buffer costs almost nothing in bytes.
Notice that two of the three options are conversations rather than code, which is why this decision belongs before generation.

Verification Jump to heading

  • Pan across a tile boundary at every zoom. Seams appear at specific zooms where a styled width crosses the buffer; testing one zoom proves nothing.
  • Check the widest styled layer specifically. A buffer adequate for roads may be inadequate for a casing or an outline drawn wider still.
  • Confirm labels near edges survive. Find a label anchored within a few pixels of a tile boundary and confirm it renders rather than flickering as you pan.
  • Measure real tiles, not the formula. Generate a dense area at two buffer values and compare actual byte sizes; the area proxy overstates the cost where geometry is sparse near edges.
  • Confirm extent does not exceed usefulness. At your maximum zoom, one grid unit should be comfortably finer than the finest detail in the source data, and no finer.

Common errors and fixes Jump to heading

Symptom Root cause One-line fix
Hairline seams on thick lines Buffer sized from a thinner layer Size from the widest styled stroke in the whole style
Labels flicker near tile edges Buffer sized from line widths only Include the largest label box in the maximum
Tiles nearly double in size Label-driven buffer applied to every layer Give labels their own layer with its own buffer
Blocky geometry at high zoom Extent too low for the maximum zoom Raise the extent until one unit is finer than the data
Deltas unexpectedly large Extent raised far beyond the visible need Lower the extent; precision nobody sees costs bytes
Seams only at one zoom Line width scales with zoom in the style Compute the buffer at the zoom with the widest rendering
Buffer ignored by the client Client clamps geometry to the tile Check the renderer honours out-of-extent coordinates

Specification reference Jump to heading

The extent field of a layer declares the width and height of the layer’s coordinate grid in tile-local units, and geometry coordinates are integers relative to the tile’s top-left origin. Coordinates outside the range 0 to extent are permitted and represent geometry retained beyond the tile boundary for rendering continuity. See the Mapbox Vector Tile specification for the extent field and the treatment of out-of-bounds geometry.

Frequently Asked Questions Jump to heading

Is 4096 always the right extent?

It is a sensible default and almost always adequate, because at the conventional 512-pixel tile rendering it gives eight grid units per pixel — finer than any display resolves. Raising it adds precision nobody sees while making deltas larger and tiles bigger. Lowering it is occasionally worth it for a deliberately coarse thematic layer, where quantisation is acceptable and every byte counts. Derive it from the ground resolution you need at your maximum zoom rather than adopting it reflexively.

How do I choose the buffer without knowing the final style?

Pick a defensible upper bound and record the assumption. A buffer of 64 grid units covers a stroke up to about sixteen screen pixels at the conventional extent, which is wider than most base-map lines. Write down what that buffer assumes, so when a designer later specifies a thirty-pixel casing, somebody can connect the resulting seams to the assumption rather than treating them as a mysterious rendering bug.

Why do labels need so much more buffer than lines?

Because a label extends outward from its anchor by half its rendered width, and a long place name is easily two hundred pixels wide where a line is ten. Sizing the buffer to accommodate labels can nearly double the geometry retained in every tile. The usual resolution is to separate concerns: keep label anchors as points in their own small layer with a generous buffer, and leave the geometry layers with a buffer sized for strokes.

Does a larger buffer always mean larger tiles?

In proportion to how much geometry actually sits near the edges, which varies enormously. A tile covering open countryside gains almost nothing from a wider buffer; a tile covering a dense city centre gains a lot, because every street near the boundary is retained further. That is why the area-based estimate is only a first-order proxy and why the decision should be confirmed by generating a dense sample at both values and comparing real bytes.

Up one level: The Mapbox Vector Tile Spec & Tile Geometry.