Merging MBTiles Files with tile-join
tile-join combines two or more MBTiles or PMTiles archives into a single tileset — and can re-filter, rename, or drop layers and join external attributes along the way — all without re-tiling from the original source data. It reads finished tiles, merges them tile-by-tile, and writes a new archive, which is far faster than regenerating with tippecanoe.
When to Use This
Use tile-join when the tiles you need already exist and you only have to recombine or trim them:
- Combining regional builds produced in parallel — tile each continent or state separately, then merge into one global archive. This sidesteps the SQLite write-lock contention of a single monolithic build by fanning the work out and joining at the end.
- Layering a basemap and overlays — merge a roads archive, a buildings archive, and a labels archive so a single tileset carries every layer the style references.
- Post-hoc attribute filtering — strip or rename properties, or join a CSV of new fields, without touching the source GeoJSON or GeoParquet.
If the geometry itself needs to change — different simplification, a new zoom range beyond what the inputs cover, or fresh source data — you must rebuild with tippecanoe instead. tile-join only recombines existing tiles; it cannot add detail that was never encoded.
Specification Detail
The flags that matter for a production merge:
| Flag | Argument | Effect |
|---|---|---|
-o |
out.mbtiles / out.pmtiles |
Output archive; extension selects the container |
-f |
— | Force overwrite of an existing output file |
-pk |
— | Do not limit tiles to 500 KB (keep every feature) |
--no-tile-size-limit |
— | Long-form of -pk |
-l |
layer |
Include only this named layer (repeatable) |
-L |
layer |
Exclude this named layer (repeatable) |
-j |
'{...}' |
Feature filter expression (GL-style) applied per layer |
-c |
join.csv |
Join CSV columns onto features by a shared key |
-x |
field |
Remove an attribute field from all features |
-pC |
— | Do not compress tiles in the output |
-pg |
— | Write PMTiles-friendly output (used with a .pmtiles -o) |
-n / -N |
name / desc |
Set the tileset name and description metadata |
The -j filter uses the same GL expression grammar as the tippecanoe -j flag, keyed by layer name, so a filter like {"roads":["<=",["get","class"],3]} keeps only major roads in the roads layer while passing every other layer through untouched. The -c CSV join matches on the first column of the CSV against a same-named feature attribute, which is the tile-level equivalent of a database join.
Production Command
Merge several regional MBTiles into one PMTiles archive, drop an unwanted layer, and join a CSV of population attributes onto place features:
# 1. Sanity-check that inputs share the same maxzoom
for f in regions/*.mbtiles; do
echo "$f: $(sqlite3 "$f" \
"SELECT value FROM metadata WHERE name='maxzoom';")"
done
# All should print the same number (e.g. 14) before you merge.
# 2. Merge regions -> single PMTiles, excluding the scratch 'debug' layer
tile-join \
-o dist/world.pmtiles \
-f \
-L debug \
-pk \
regions/north_america.mbtiles \
regions/europe.mbtiles \
regions/asia.mbtiles \
regions/africa.mbtiles \
regions/south_america.mbtiles \
regions/oceania.mbtiles
# 3. Join a CSV of attributes onto the 'places' features by shared key
# places.csv header: iso_a2,population,capital
tile-join \
-o dist/world_enriched.pmtiles \
-f \
-c data/places.csv \
dist/world.pmtiles
# 4. Re-check the per-tile size budget after merging
pmtiles show dist/world_enriched.pmtiles
The CSV passed to -c must have a header row; its first column is the join key and must exactly match an existing attribute name on the features you want to enrich. Rows with no matching feature are ignored, and features with no matching CSV row keep their original attributes unchanged.
Interaction Effects
Feeds the same delivery path. A merged archive is an ordinary tileset — whether you write .mbtiles or .pmtiles, it flows into exactly the same serving and CDN pipeline as a freshly built one. Writing directly to .pmtiles here means the merge output is ready for single-file, range-request delivery with no separate pmtiles convert step.
Re-check tile size after every merge. The single most important interaction: merging is additive. Two archives that were each comfortably under the 500 KB per-tile ceiling can produce a merged tile that blows past it, because their features now share the same tile. If you passed -pk to preserve everything, nothing was dropped — so an oversized tile survives into output and MapLibre may refuse it. Budget management here connects directly to controlling tile size with drop and coalesce flags at the tippecanoe stage.
Ties to attribute filtering. The -x, -l, -L, and -j flags let you do at merge time what you might otherwise do during generation. If your inputs carry attributes your style never reads, stripping them with -x during the join shrinks tiles the same way dropping unused attributes to reduce tile size does at build time — useful when you cannot re-run the original build but still need smaller tiles. The full set of generation flags these mirror is catalogued in Tippecanoe CLI Fundamentals.
Performance Impact
tile-join is I/O-bound tile copying, not geometry processing, so it runs dramatically faster than a full rebuild. Merging a set of regional archives totaling a few gigabytes typically completes in single-digit minutes, versus the tens of minutes to hours a from-source tippecanoe run over the same data would take. Because it never re-simplifies or re-projects, CPU stays low and the wall-clock cost scales roughly with total tile bytes read and written.
The performance risk is not speed but correctness of tile size. When -pk keeps every feature, merged tiles in dense areas can grow well past 500 KB; a client will then either render slowly or reject the tile. Dropping -pk lets tile-join re-apply the size limit and shed features from overfull tiles, trading completeness for a safe budget. There is no free lunch: if merged density is genuinely too high, you either accept dropped features at the join or return to tippecanoe and rebuild with coalescing.
Common Mistakes
Colliding layer names silently overwrite. If two inputs both contain a layer named roads, tile-join merges their features into one roads layer — and where features occupy the same tile, later inputs can overwrite earlier ones with no warning. Symptom: features from the first archive vanish in overlapping regions. Fix: rename layers before merging, or select explicitly:
# Keep only the intended layer from each input to avoid a silent collision
tile-join -o merged.mbtiles -f \
-l roads_primary basemap.mbtiles \
-l roads_local overlay.mbtiles
Exceeding 500 KB after the merge. Merging two dense archives with -pk produces oversized tiles. Symptom: MapLibre logs a tile that is too large, or rendering stalls on busy tiles. Diagnose the largest tiles, then re-merge without -pk so the size limit re-applies:
# Find the biggest tiles in a merged MBTiles
sqlite3 merged.mbtiles \
"SELECT zoom_level, tile_column, tile_row, length(tile_data) AS bytes
FROM tiles ORDER BY bytes DESC LIMIT 5;"
# If any bytes value is near or over 500000, re-run tile-join WITHOUT -pk.
Mismatched maxzoom between inputs. Merging an archive built to z14 with one built to z12 yields a tileset whose high-zoom tiles only exist where the z14 input covered — the z12 regions simply stop at zoom 12. Symptom: empty tiles above z12 in some areas after the merge. Fix: confirm every input reports the same maxzoom in its metadata (the check in step 1 of the command block above) and rebuild any laggards to the target zoom before joining.
Parent: MBTiles Architecture & Limits
Related
- Dropping Unused Attributes to Reduce Tile Size — strip properties at build or merge time to keep tiles under the 500 KB budget.
- Resolving SQLite Locks in Large MBTiles Generation — why fanning builds out and joining with tile-join sidesteps write-lock contention.
- Tippecanoe CLI Fundamentals — the generation flags whose behavior tile-join’s include, exclude, and filter options mirror.