Splitting a Planet File into Regional Extracts Jump to heading
Produce a dozen regional .osm.pbf files from one parent extract in a single pass, instead of reading the parent once per region and turning a twenty-minute job into a four-hour one.
Prerequisites Jump to heading
Conceptual minimum Jump to heading
osmium extract accepts a --config file listing many outputs. It reads the parent once and writes every output as it goes, which is the entire point: the extraction arithmetic is cheap and reading tens of gigabytes off disk is not.
The saving is linear in the number of regions, and it is the difference between a job that fits in a nightly window and one that does not. It also removes a subtler cost — twelve runs against a network-mounted parent transfer the file twelve times.
The config format Jump to heading
{
"directory": "/data/extracts",
"extracts": [
{
"output": "ireland.osm.pbf",
"description": "Republic of Ireland",
"polygon": { "file_name": "/data/boundaries/ireland.poly", "file_type": "poly" }
},
{
"output": "scotland.osm.pbf",
"polygon": { "file_name": "/data/boundaries/scotland.geojson", "file_type": "geojson" }
},
{
"output": "greater-london.osm.pbf",
"bbox": [-0.51, 51.28, 0.33, 51.69]
}
]
}
directory is the shared output directory; each output is a filename within it. Region geometry comes from one of three keys, and the choice is the same one discussed in the parent topic, Extract Clipping & Boundary Polygons.
Two config details matter more than they look. The strategy is set once on the command line and applies to every extract in the run, so a batch that mixes regions needing smart with regions where complete_ways would do must either use smart throughout or be split into two runs. And description is not decoration: it is written into the output file’s header, which is the only place the reason for a cut survives.
Runnable solution Jump to heading
#!/usr/bin/env python3
"""Cut many regions from one parent in a single osmium pass, then verify each output."""
from __future__ import annotations
import json
import logging
import shutil
import subprocess
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
logger = logging.getLogger(__name__)
# Rough output size as a fraction of the parent, used only for the disk pre-flight.
SIZE_FRACTION = 0.05
def preflight(config: dict, parent: Path) -> None:
"""Refuse to start a run that cannot finish for want of disk."""
out_dir = Path(config["directory"])
out_dir.mkdir(parents=True, exist_ok=True)
n = len(config["extracts"])
need = int(parent.stat().st_size * SIZE_FRACTION * n * 1.1)
free = shutil.disk_usage(out_dir).free
logger.info("pre-flight: %d region(s), ~%.1f GB needed, %.1f GB free",
n, need / 1e9, free / 1e9)
if free < need:
raise RuntimeError(f"insufficient disk: need ~{need/1e9:.1f} GB, have {free/1e9:.1f} GB")
for e in config["extracts"]:
poly = e.get("polygon", {}).get("file_name")
if poly and not Path(poly).exists():
raise FileNotFoundError(f"boundary missing for {e['output']}: {poly}")
def split(config_path: Path, parent: Path, strategy: str = "smart") -> None:
subprocess.run(
["osmium", "extract", "--config", str(config_path),
"--strategy", strategy, "--overwrite", str(parent)],
check=True,
)
def verify_all(config: dict) -> None:
"""Every output, not just the last one — a batch run hides a single bad boundary."""
out_dir = Path(config["directory"])
failures: list[str] = []
for entry in config["extracts"]:
path = out_dir / entry["output"]
if not path.exists():
failures.append(f"{entry['output']}: not written")
continue
info = json.loads(subprocess.run(
["osmium", "fileinfo", "--extended", "--json", str(path)],
capture_output=True, text=True, check=True,
).stdout)
nodes = info["data"]["count"]["nodes"]
if nodes == 0:
failures.append(f"{entry['output']}: zero nodes — check the boundary")
else:
logger.info("%-28s %12d nodes %8.1f MB",
entry["output"], nodes, path.stat().st_size / 1e6)
if failures:
raise RuntimeError("verification failed:\n " + "\n ".join(failures))
if __name__ == "__main__":
cfg_path = Path("extracts.json")
cfg = json.loads(cfg_path.read_text())
parent_file = Path("planet-latest.osm.pbf")
preflight(cfg, parent_file)
split(cfg_path, parent_file)
verify_all(cfg)
Step-by-step walkthrough Jump to heading
preflight does two things that turn a class of overnight failures into an immediate one. It estimates total output size and compares it against free disk, and it checks that every boundary file named in the config exists. Both failures otherwise appear hours into the run, after most of the work is done, and osmium will have written partial outputs by then.
split is one subprocess call. --strategy on the command line applies to every entry; --overwrite is needed for a repeatable job, since osmium will not clobber an existing output.
verify_all is the part most implementations omit. A batch run exits zero as a whole, so a region whose boundary was written in the wrong axis order produces an empty file that nothing complains about. Iterating every declared output and asserting a non-zero node count catches it before the next stage consumes it.
Verification Jump to heading
Expect one log line per region with a plausible node count and file size. Two patterns in that output indicate a problem: a region whose node count is orders of magnitude away from its neighbours of similar area, and a file size close to zero. Both mean a bad boundary rather than a sparse region.
ls -la /data/extracts/*.osm.pbf | awk '{print $5, $9}' | sort -n | head -3
If the smallest outputs are a few hundred bytes, those boundaries are wrong. A genuinely small region still produces a file in the megabytes.
Common errors and fixes Jump to heading
| Symptom | Root cause | Fix |
|---|---|---|
| Some outputs missing, no error | Disk filled part-way through | Pre-flight the total; write to a dedicated volume |
| One empty file among many good ones | That boundary has swapped axes | Verify every output; fix the boundary |
Config file error at startup |
Trailing comma or a file_type typo |
Validate the JSON before invoking osmium |
| Fewer files than regions | Two entries share an output name |
Make output names unique |
| Killed after hours with no output | smart holds an id set per region |
Split into several runs of fewer regions |
| Every output is tiny | Parent does not cover the boundaries | Check the parent’s own bbox first |
Specification reference Jump to heading
osmium extract --config FILEreads a JSON document with an optionaldirectoryand a list ofextracts. Each entry needs anoutputand exactly one ofbbox,polygonormultipolygon. The input is read once and all extracts are written concurrently;--strategyapplies to the whole run.
Budgeting a split Jump to heading
Two resources bound a multi-region run, and they bind in opposite directions, which is why a run that fits on one machine fails on another with more disk.
Disk is the easy one. Regional extracts add up to well under the parent — a planet file split into every country produces roughly 55 to 65 percent of the planet’s own size, because the strategies duplicate only the objects near boundaries. Estimating five percent of the parent per country-sized region, as the pre-flight above does, is deliberately generous and errs toward refusing a run that would in fact have fitted. That is the right direction to err: a refusal costs a minute, and a disk that fills two hours in costs the run.
Memory is the harder one because it does not scale with output size. Under the smart strategy each region under construction holds a set of the object identifiers it has decided to keep, and that set is sized by the number of objects near that region’s boundary. A compact region with a short boundary is cheap; a long coastal country, or an administrative area with many enclaves, is not. Twenty regions cut concurrently from a continent will sit comfortably in tens of gigabytes; the same twenty cut from the planet will not, because every identifier set is drawn from a hundred times as many candidate objects.
The practical consequence is a two-stage split for anything planet-scale. Cut the planet into continents once, in a run of six or seven regions, then cut countries from their continent in separate runs. The parent is read eight times rather than once, but each read is of a much smaller file and no run holds more than a handful of identifier sets. On the measurements above that shape runs in about ninety minutes for a full country-level split of the planet, against a single-stage run that does not complete at all on a 64 GB machine.
One further economy is worth knowing. If the same regions are cut every week, keeping the continent-level intermediates means the weekly job never touches the planet file: only the continents that actually changed need re-cutting from a fresh planet, and everything downstream of an unchanged continent can be skipped entirely. That turns a full split into an incremental one without any additional tooling.
Frequently Asked Questions Jump to heading
How many regions can go in one run?
Disk and memory decide it, not the tool. Every region being cut concurrently holds its own identifier set under smart, so budget roughly a gigabyte per region on a continent-sized parent and rather more on a planet. Twelve to twenty regions in a run is comfortable on a machine with 64 GB; beyond that, splitting into several runs still reads the parent far fewer times than cutting each region separately.
Can I cut overlapping regions in the same run?
Yes. The regions are independent — an object inside two boundaries is written to both outputs — so overlapping extracts such as a country and one of its cities cost no more than disjoint ones. This is often the cheapest way to produce a nested set of extracts, because the alternative is cutting the city out of the country afterwards, which reads the country file again.
Should I cut the regions I need from the planet, or from continents first?
From continents, if you need more than a handful of regions per continent. A two-stage split — planet to continents once, then continents to countries — reads the planet once and each continent once, whereas cutting fifty countries directly from the planet in one configured run also reads the planet once but holds fifty identifier sets in memory at the same time. The two-stage version trades a little extra disk for a much lower memory ceiling.
Does a batched run write outputs incrementally or at the end?
Incrementally, as the parent is read. That is why a run interrupted by a full disk leaves several complete-looking files behind: they are not truncated, they are simply missing everything that came after the failure point. Nothing in the file marks it as partial, which is exactly why the verification step compares node counts rather than trusting the exit code.
Related Jump to heading
- Extract Clipping & Boundary Polygons — the topic this procedure belongs to.
- Clipping an OSM Extract with a .poly Boundary — the single-region case, with the boundary writer.
- Choosing complete_ways vs smart in osmium extract — the strategy that applies to the whole run.
- Extracting Metadata from OSM Planet Files — checking the parent’s coverage before cutting.
- Error Handling in Large OSM Extracts — how a partial batch should be reported.
Up one level: Extract Clipping & Boundary Polygons.