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.
Runnable solution Jump to heading
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:
#!/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
- Classify before ranking. The road table maps a closed vocabulary rather than every possible
highwayvalue, with an explicit default so an unknown value gets the deepest zoom rather than an exception. - 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.
- 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.
- Band areas rather than computing a formula. A table of thresholds is reviewable by a cartographer; a continuous function is not.
- 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.
- 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.
- 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.
- 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.
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:minzoomandtippecanoe:maxzoomproperties 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.
Related Jump to heading
- Building OSM Tiles with Tippecanoe — the parent topic and the reduction mechanisms this tuning replaces.
- Generating MBTiles from OSM GeoJSON — the end-to-end run this ranking feeds.
- Simplifying OSM Geometry per Zoom Level — the other half of making a low-zoom tile fit.
- Mapping OSM Tags to a Fixed Schema with YAML — the classification step expressed as configuration.
- Cartographic Generalization of OSM Data — where rank fits among the other generalization tools.
Up one level: Building OSM Tiles with Tippecanoe.