Tuning Tippecanoe Zoom and Feature Dropping Jump to heading

Stop the generator choosing which features survive a crowded tile, by making sure the tile was never crowded — with minimum zooms computed from what each OSM feature actually is.

Prerequisites Jump to heading

Conceptual minimum Jump to heading

Tippecanoe reduces a tile in two entirely different ways, and only one of them is yours.

Declared zoom ranges are a property of each feature. A feature with a minimum zoom of 9 simply does not exist in tiles below zoom 9. This is a decision you make, it is recorded in the data, and it is reviewable.

Automatic dropping is a property of the tile. When a tile exceeds the size budget, features are removed until it fits, chosen by a spatially uniform heuristic that knows nothing about a motorway outranking a driveway. This is a decision the generator makes on your behalf, it is recorded only in build output, and it produces the complaint that features vanish unpredictably.

The strategy is therefore not to disable dropping — it is a useful safety net — but to make it rarely fire, by ensuring each zoom’s tiles contain roughly the number of features that zoom can afford.

From OSM tags to a per-feature minimum zoom Four steps. The classify step maps raw OSM tags onto a small closed vocabulary such as motorway, trunk, primary and residential. The rank step assigns each class a numeric importance, optionally adjusted by a size or population attribute. The zoom step maps rank onto a minimum zoom using a table that is reviewed rather than tuned by trial and error. The attach step writes the reserved minimum and maximum zoom properties onto the feature so the generator honours the decision. Four steps, all of them before the generator runs classify tags to a vocabulary a dozen classes, not 400 rank class plus magnitude area, length, population map to zoom a reviewed table not trial and error attach reserved properties the decision is data Writing the zoom onto the feature makes the decision auditable: anybody can query which features appear at which zoom.
Because the rank lives in the data, a cartographic change becomes a table edit rather than a pipeline change.

Runnable solution Jump to heading

python
from __future__ import annotations

import json
import logging
import math
import sys
from typing import Any, Iterator

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

# Road classes ordered by importance, with the zoom each first appears at.
ROAD_MINZOOM: dict[str, int] = {
    "motorway": 4, "trunk": 5, "primary": 7, "secondary": 9,
    "tertiary": 11, "unclassified": 12, "residential": 13,
    "service": 14, "track": 14, "path": 14,
}
# Area thresholds in square metres -> minimum zoom for polygon features.
AREA_BANDS: list[tuple[float, int]] = [
    (1e9, 3), (1e8, 5), (1e7, 7), (1e6, 9),
    (1e5, 11), (1e4, 12), (1e3, 13),
]
DEFAULT_MINZOOM = 14
MAXZOOM = 14


def road_minzoom(tags: dict[str, Any]) -> int | None:
    value = tags.get("highway")
    if value is None:
        return None
    base = ROAD_MINZOOM.get(value, DEFAULT_MINZOOM)
    # A named road with a reference number is usually more important than its
    # class alone suggests; promote it by one level, never past motorway.
    if tags.get("ref"):
        base = max(3, base - 1)
    return base


def area_minzoom(area_m2: float) -> int:
    for threshold, zoom in AREA_BANDS:
        if area_m2 >= threshold:
            return zoom
    return DEFAULT_MINZOOM


def place_minzoom(tags: dict[str, Any]) -> int | None:
    if "place" not in tags:
        return None
    population = tags.get("population")
    try:
        people = float(population) if population else 0.0
    except ValueError:
        people = 0.0
    if people <= 0:
        return {"city": 6, "town": 9, "village": 11}.get(tags["place"], 13)
    # Population spans six orders of magnitude; a log scale keeps the mapping sane.
    return max(3, min(13, int(14 - 1.6 * math.log10(max(people, 10.0)))))


def assign(feature: dict[str, Any]) -> dict[str, Any]:
    props = feature.get("properties", {})
    minzoom = (road_minzoom(props)
               or place_minzoom(props)
               or (area_minzoom(float(props["area_m2"]))
                   if props.get("area_m2") else None)
               or DEFAULT_MINZOOM)
    props["tippecanoe:minzoom"] = int(minzoom)
    props["tippecanoe:maxzoom"] = MAXZOOM
    # Keep the rank as a real attribute too: the style may want it for widths.
    props.setdefault("rank", int(minzoom))
    feature["properties"] = props
    return feature


def stream(lines: Iterator[str]) -> Iterator[str]:
    histogram: dict[int, int] = {}
    for line in lines:
        line = line.strip()
        if not line:
            continue
        feature = assign(json.loads(line))
        z = feature["properties"]["tippecanoe:minzoom"]
        histogram[z] = histogram.get(z, 0) + 1
        yield json.dumps(feature, separators=(",", ":"))
    for z in sorted(histogram):
        logger.info("minzoom %2d: %d feature(s)", z, histogram[z])


if __name__ == "__main__":
    for out in stream(sys.stdin):
        print(out)

Then run the generator with dropping left as a safety net rather than a control:

bash
#!/usr/bin/env bash
set -euo pipefail

python3 assign_zoom.py < roads.geojsonseq > roads.ranked.geojsonseq

tippecanoe \
  --output=osm.mbtiles --force \
  --maximum-zoom=14 --minimum-zoom=0 \
  --named-layer=transportation:roads.ranked.geojsonseq \
  --named-layer=landuse:landuse.ranked.geojsonseq \
  --coalesce --reorder \
  --maximum-tile-bytes=500000 \
  2>&1 | tee build.log

# Dropping should be rare. If it is not, the zoom table is too generous.
grep -ci "dropping" build.log || echo "no dropping reported"

Step-by-step walkthrough Jump to heading

  1. Classify before ranking. The road table maps a closed vocabulary rather than every possible highway value, with an explicit default so an unknown value gets the deepest zoom rather than an exception.
  2. Let a secondary signal adjust the rank. A road carrying a reference number is usually more significant than its class alone implies, so it is promoted one level — with a floor so nothing outranks a motorway.
  3. Use a log scale for population. Settlement populations span six orders of magnitude, and a linear mapping puts every village in one bucket and every city in another.
  4. Band areas rather than computing a formula. A table of thresholds is reviewable by a cartographer; a continuous function is not.
  5. Fall through in priority order. Roads, then places, then area, then a default. Each feature matches exactly one rule, and the default guarantees no feature is left without a zoom.
  6. Keep the rank as a real attribute. The style often wants it for line widths, and exposing it avoids the style re-deriving the same classification from raw tags.
  7. Log the histogram. The count of features per minimum zoom is the single most useful review artefact: a zoom holding ten times more features than the one below it is where crowding will appear.
  8. Check the build log for dropping. If dropping is still frequent, the zoom table is too generous at that level — the histogram tells you which one.
Feature counts by assigned minimum zoom for a country road network Six minimum zoom bands with the number of road features assigned to each. Zoom four holds a few thousand motorways. Zoom seven holds tens of thousands of primary roads. Zoom nine holds around a hundred thousand secondary roads. Zoom eleven holds several hundred thousand tertiary roads. Zoom thirteen holds a few million residential streets. Zoom fourteen holds the remainder, dominated by service roads and tracks. A note observes that each band should be roughly four times the one above it, matching the tile count growth. Each band should be about four times the one above minzoom 4 about 3,000 minzoom 7 about 30,000 minzoom 9 about 110,000 minzoom 11 about 420,000 minzoom 13 about 1.8 million minzoom 14 about 2.6 million A band far larger than four times its predecessor is where tiles will crowd and the dropping heuristic will start choosing for you.
Reading this histogram after every change is faster than inspecting tiles, and it predicts exactly where crowding appears.
Three sources of a feature's rank and when each is the right basis Three panels. Class-based ranking maps a tag value such as a road classification directly to a zoom and is right whenever the tagging already encodes importance. Magnitude-based ranking derives the zoom from a measured quantity such as polygon area or route length and is right when features of one class vary enormously in size. Attribute-based ranking uses a separate tag such as population or a reference number and is right when the class alone under-describes significance, though it must handle missing and unparseable values. Three bases for a rank, each with its own failure Class Tag value to zoom directly Roads, railways, boundaries Tagging already ranks them Fails on unknown values Always set a default Magnitude Area, length or extent Lakes, forests, built-up areas One class, huge size range Fails on missing geometry Band it, do not compute it Attribute Population, reference number Settlements, numbered routes Class alone under-describes Fails on unparseable values Coerce defensively Most real layers use two of these: a class baseline adjusted by a magnitude or an attribute, with clamps at both ends.
Naming which basis a layer uses is what makes the zoom table reviewable by somebody who did not write it.

Verification Jump to heading

  • The histogram roughly quadruples per level. A band much larger than four times the one above it predicts crowding at that zoom.
  • The build log reports little or no dropping. Occasional dropping in the densest city tiles is acceptable; systematic dropping means the table is too generous.
  • A known feature appears where intended. Pick a specific secondary road and confirm it is present at its declared zoom and absent below it.
  • No feature lacks a zoom. Count features whose reserved minimum zoom property is missing; it must be zero.
  • Rank survives into the tiles. Decode a tile and confirm the rank attribute is present for the style to use.

Common errors and fixes Jump to heading

Symptom Root cause One-line fix
Dropping still frequent Zoom table too generous at one level Read the histogram; push the oversized band deeper
Every village at one zoom Population mapped linearly Use a logarithmic mapping for population
Unknown road class crashes Table lookup without a default Default unknown values to the deepest zoom
Motorway missing at low zoom Promotion applied without a floor Clamp the promoted value so nothing outranks the top class
Style re-derives classification Rank not exposed as an attribute Keep the rank as a real property alongside the reserved ones
Reserved properties in the output Wrong property names used They must be the exact reserved names to be consumed
Features present below their zoom Properties written as strings Write the zoom values as integers, not strings

Specification reference Jump to heading

Tippecanoe reads per-feature tippecanoe:minzoom and tippecanoe:maxzoom properties from the input GeoJSON and uses them to limit the zoom levels at which each feature is included, in preference to the automatic feature-dropping applied to keep tiles within the configured maximum size. These reserved properties are consumed and do not appear in the output tiles. See the Tippecanoe documentation for the full list of reserved properties and the dropping options.

Frequently Asked Questions Jump to heading

Should I disable automatic dropping entirely?

No. It is a useful safety net for the genuinely exceptional tile — a dense city centre where even a well-ranked layer occasionally exceeds the budget. What you should avoid is relying on it as the primary reduction mechanism, because it chooses victims without any notion of importance. Aim for a zoom table generous enough that dropping fires rarely, and treat a build log full of dropping as a signal to revisit the histogram.

How do I choose the minimum zoom for each class?

Start from the tile count arithmetic. Each zoom level has four times as many tiles as the one above, so a band roughly four times larger than its predecessor keeps per-tile feature counts stable. Assign the classes in importance order, check the resulting histogram, and move any band that breaks the pattern. That converges in two or three iterations and is far more reliable than adjusting numbers until the map looks right in one place.

Why use a logarithmic scale for population?

Because settlement populations span six orders of magnitude, from a hamlet of forty people to a city of twenty million. A linear mapping puts almost every settlement in the bottom bucket and a handful in the top, which produces a map where nothing appears until you zoom well in and then everything appears at once. A logarithmic mapping spreads settlements evenly across zoom levels, which is what a reader expects.

Can the rank be used for styling as well as dropping?

Yes, and it should be. Keeping the computed rank as an ordinary attribute lets the style scale line widths and label sizes from it directly, instead of re-deriving the same classification from raw tag values in the style sheet. That keeps one definition of importance in one place, so a cartographic change is a table edit rather than a change in two systems that must be kept in step.

Up one level: Building OSM Tiles with Tippecanoe.