Choosing minzoom for Sparse Datasets

For a sparse layer — weather stations, ferry routes, a few hundred sites across a country — the tile-size budget never binds, so the usual density arithmetic gives no answer at all. The shallowest useful zoom is decided by two other things: the zoom at which the features stop being legible as individuals, and the number of near-empty tiles you are willing to generate and serve below it.

When to Use This

Any layer where the whole dataset would fit in a handful of tiles at high zoom. Monitoring networks, retail locations, incident reports, protected areas, transport termini. The symptom that brings people here is usually a tileset with tens of thousands of tiles that are almost all empty, or a map where a layer is technically present at z4 and reads as noise.

Specification Detail

Flag Scope Effect
-Z / --minimum-zoom Whole build The shallowest zoom generated
minzoom in vector_layers Per layer, metadata Documentation; does not suppress requests
minzoom on a style layer Runtime The zoom below which the layer is not drawn
minzoom on a style source Runtime The zoom below which no tile is requested at all

The distinction in the last two rows is the one that matters operationally. Setting minzoom on a style layer stops the drawing and not the fetching; setting it on the source stops the fetching. For a sparse layer in its own tileset, the source-level setting is what removes the wasted requests.

How many tiles each starting zoom producesTile counts for a sparse 850-feature dataset generated from zoom 0, 4, 6 and 8 up to zoom 12, showing the count exploding well before the features become numerous.TILES GENERATEDtiles produced for 850 features over one country-Z0 (from world view)21,845-Z421,504-Z620,480-Z816,384
The features never change. Almost all of these tiles are empty or near-empty, and every one of them is an object to store, serve and cache.

That table looks like a weak argument for raising -Z, and it is: the deep zooms dominate the count regardless. The real saving from a higher minzoom is not in tile count but in requests — a world-view map that does not request this layer at all makes eight to sixteen fewer requests per pan at low zoom.

The Two Criteria That Actually Decide It

Legibility

At some zoom, a sparse layer’s features are closer together on screen than the symbol used to draw them, and the layer becomes a clump rather than a set of locations. That is the shallowest zoom worth drawing it at, and it is a function of the symbol size and the closest pair of features, not the average spacing.

python
import math
from itertools import combinations

def screen_separation_px(lon1, lat1, lon2, lat2, zoom, tile_px=256):
    """Distance in screen pixels between two coordinates at a zoom."""
    def project(lon, lat):
        x = (lon + 180) / 360 * (2 ** zoom) * tile_px
        s = math.sin(math.radians(lat))
        y = (0.5 - math.log((1 + s) / (1 - s)) / (4 * math.pi)) * (2 ** zoom) * tile_px
        return x, y
    (x1, y1), (x2, y2) = project(lon1, lat1), project(lon2, lat2)
    return math.hypot(x2 - x1, y2 - y1)


def legible_minzoom(points, symbol_px=14, percentile=0.02):
    """Shallowest zoom at which all but the closest `percentile` of pairs are separated."""
    for zoom in range(0, 20):
        seps = [screen_separation_px(*a, *b, zoom) for a, b in combinations(points, 2)]
        seps.sort()
        cutoff = seps[int(len(seps) * percentile)]
        if cutoff >= symbol_px:
            return zoom
    return 19


stations = [(-0.12, 51.50), (-2.24, 53.48), (-3.19, 55.95), (-1.55, 53.80)]
print(legible_minzoom(stations))   # 6

Allowing a small percentile of pairs to overlap is deliberate: insisting that every pair be separated pushes the answer several zooms deeper because of one cluster, and clusters are exactly what a renderer’s collision detection is for.

Request cost below that zoom

Once the legible zoom is known, the question is whether to generate anything below it. Generating down to z0 costs almost nothing in storage — low zooms are few tiles — and it does cost requests, because a client with no source-level minzoom will ask for them.

The clean arrangement is to generate from the legible zoom and declare it on the source, so that no request is made below it. The alternative — generating everything and hiding the layer in the style — leaves the requests in place.

Which of the two criteria binds for your layerA decision tree distinguishing layers limited by symbol collision, layers that should appear at world view for orientation, and layers whose shallow zooms should be suppressed at the source.DECIDEWhat should this layer do when the reader iszoomed out?Individuallymeaningful at anyzoomLow minzoom, and acceptthe clumpingOnly meaningfulonce separatedSet minzoom to the legiblezoom, on the sourceNeeded fororientation atworld viewAggregate into a separatelow-zoom layerInteractive lookuponlyDo not tile it — query it

Production Command

bash
tippecanoe \
  --output stations.pmtiles \
  --layer stations \
  --minimum-zoom 6 \
  --maximum-zoom 12 \
  --include name \
  --include operator \
  --force \
  stations_4326.geojson

And the source declaration that stops the requests:

json
{
  "sources": {
    "stations": {
      "type": "vector",
      "url": "https://tiles.example.com/v43/stations.json",
      "minzoom": 6,
      "maxzoom": 12
    }
  }
}

The minzoom on the source must not be shallower than the build’s -Z, or the client will request tiles that were never generated and receive 404s at world view — a common and confusing symptom, because the map looks fine as soon as anyone zooms in.

Screen separation between the closest features, by zoomPixel separation between the second-closest pair of features in a sparse dataset plotted from zoom 3 to zoom 11, crossing the symbol size around zoom 6.LEGIBILITY5003752501250z3z4z5z6z7z9z11zoom level14 px symbol
The curve crosses the symbol size at z6, which is the shallowest zoom at which the layer reads as separate locations rather than a clump.

Interaction Effects

With overzoom. A sparse layer benefits from a low maxzoom and overzoom more than a dense one does, because points do not gain detail with zoom. Generating to z12 and overzooming to z16 is usually indistinguishable from generating to z16 — see overzooming versus generating higher max zoom.

With clustering. If the layer must appear at world view, an aggregated companion layer — counts per region, generated separately — reads far better than the raw points and costs a fraction of the tiles.

With combining into one tileset. A sparse layer alongside dense ones in a single tileset inherits the tileset’s zoom range, so its shallow zooms are generated whether or not they are wanted. That is usually an acceptable trade for the reduction in sources, since the empty tiles are tiny.

Common Mistakes

Deriving minzoom from the size budget. For a sparse layer the budget is never reached, so the calculation returns z0 and the answer is useless.

Setting minzoom on the style layer only. Stops the drawing, not the fetching. The requests continue at every zoom.

Using average spacing rather than the closest pairs. The average is dominated by the wide empty areas; what makes a layer illegible is the tight cluster.

Declaring a source minzoom shallower than the build. Produces 404s at exactly the zooms where the layer is least important, which is why it goes unnoticed for a long time.

FAQ

Should a sparse layer live in its own tileset?

Often yes, because its zoom range and update cadence differ from a basemap’s. The cost is one more source and its round trip, so the trade is worth making when the layer updates on its own schedule and not otherwise.

What symbol size should the legibility calculation use?

The rendered size including any halo or padding, not the icon’s intrinsic size. A 12-pixel circle with a 2-pixel halo occupies 16 pixels, and using 12 gives an answer one zoom too shallow.

Does an empty tile cost anything?

Very little individually — a few dozen bytes — but they are still objects to store, requests to serve and cache entries to hold. The cost is in the request count at low zoom, not the bytes.

Can I set a different minzoom per layer within one tileset?

Tippecanoe supports per-layer zoom ranges through separate invocations merged with tile-join, or through -Z and -z applied per input file. Within a single invocation over one input, the range applies to the whole build.