How to Tag and Release Versioned OpenStreetMap Extracts
Producing a deterministic, auditable OSM extract release requires pairing a semantic version tag with an immutable spatial snapshot and a machine-readable manifest β part of the broader release tagging strategies for spatial basemaps that govern how geospatial teams distribute reference data.
Concept & Context
OpenStreetMap data mutates continuously through thousands of daily edits. A .osm.pbf file downloaded today will differ from one downloaded tomorrow, even for the same geographic boundary. Without explicit versioning, downstream GIS pipelines β routing engines, tile servers, environmental models β silently diverge as teams pull extracts at different moments.
The solution is dual identification: every extract carries a human-readable semantic version (vMAJOR.MINOR.PATCH) and a machine-readable temporal anchor embedded as the OSM replication sequence number (osmosis_replication_sequence_number) and replication timestamp (osmosis_replication_timestamp). These two fields together let any consumer reconstruct the exact state of the planet file at extraction time, independent of wall-clock dates.
This approach builds directly on the branching and merge strategies for spatial datasets that govern how feature work is isolated before a release is cut. Teams following those conventions should create a dedicated release candidate branch, run validation, and only tag once CI gates pass β the same discipline applied here at the artifact level.
The diagram below shows the full lifecycle from a raw planet mirror to a versioned, verified artifact in the registry.
Core Algorithmic Pipeline
Step 1 β Resolve the semantic version and replication anchor
Before touching any files, decide which version component increments:
- MAJOR: extraction boundary (
--polygon), coordinate reference system (EPSG:4326β other), or output format (.osm.pbfβ.geojson) changed. - MINOR: filter expressions, tag transformations, or attribute enrichment rules changed.
- PATCH: upstream OSM replication feed advanced; extraction logic is unchanged.
Record the current replication sequence number before the extract runs:
osmium fileinfo -g header.option.osmosis_replication_sequence_number source.osm.pbf
Store this integer in a variable; it becomes the canonical temporal anchor in the manifest.
Step 2 β Extract the regional PBF snapshot
Use osmium extract with a --polygon GeoJSON or Osmosis poly file to enforce a deterministic geographic boundary. Always specify --strategy=complete-ways to prevent split ways at the boundary edge, which would fail topology checks downstream:
osmium extract \
--polygon boundary/city-of-nairobi.geojson \
--strategy=complete-ways \
--output releases/nairobi-v1.4.2.osm.pbf \
planet/africa-latest.osm.pbf
Tag the boundary file itself in version control. If the polygon changes between releases, that is a MAJOR increment regardless of how small the geometry shift appears.
Step 3 β Generate the VERSION.json manifest
The manifest is the machine-readable contract consumed by downstream pipelines. It must travel alongside the artifact in every distribution channel.
Step 4 β Compute the SHA-256 checksum
Write a .sha256 sidecar file in the GNU sha256sum format so consumers can run sha256sum -c without parsing JSON:
sha256sum releases/nairobi-v1.4.2.osm.pbf > releases/nairobi-v1.4.2.sha256
Step 5 β Commit metadata and annotate the Git tag
Stage only the manifest and checksum β never the binary PBF, which belongs in Git LFS or object storage. An annotated tag (not a lightweight tag) stores the tagger identity and timestamp in the object database, satisfying audit trail requirements described in security boundaries for spatial repositories:
git add releases/nairobi-v1.4.2.json releases/nairobi-v1.4.2.sha256
git commit -m "release: nairobi v1.4.2"
git tag -a v1.4.2 -m "OSM extract nairobi v1.4.2 β PATCH refresh, seq 4182901"
git push origin main --follow-tags
Step 6 β Push binary artifacts to remote storage
For repositories using Git LFS:
git lfs track "*.osm.pbf"
git add .gitattributes
git commit -m "chore: track PBF files with Git LFS"
git lfs push --all origin
For S3-compatible object storage, sync with the structured {region}-{version} naming convention so pipelines can discover artifacts programmatically:
aws s3 sync ./releases/ s3://spatial-artifacts/osm-extracts/ \
--exclude "*" \
--include "nairobi-v1.4.2.*" \
--acl bucket-owner-full-control
Working Implementation
The script below integrates all six steps. It expects osmium, git, and git lfs in $PATH and a pre-downloaded source PBF. Outputs land in a ./releases/ directory: the extracted PBF, a VERSION.json manifest, and a .sha256 sidecar.
#!/usr/bin/env python3
"""
Automated OSM extract versioning pipeline.
Usage:
python tag_release.py <region> <source.pbf> <boundary.geojson> <vX.Y.Z>
Produces:
releases/{region}-{version}.osm.pbf β extracted snapshot
releases/{region}-{version}.json β VERSION.json manifest
releases/{region}-{version}.sha256 β SHA-256 sidecar
"""
import hashlib
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
def run(cmd: list[str], cwd: str | None = None) -> str:
"""Run a subprocess and return stripped stdout; raise on non-zero exit."""
result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=cwd)
return result.stdout.strip()
def sha256_file(path: Path) -> str:
"""Compute SHA-256 hex digest in 8 KiB chunks (streaming β safe for large PBFs)."""
h = hashlib.sha256()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(8192), b""):
h.update(chunk)
return h.hexdigest()
def osm_replication_meta(pbf_path: Path) -> dict[str, str]:
"""
Read OSM replication timestamp and sequence number embedded in the PBF header.
Both fields are written by Geofabrik and the osmosis replication toolchain.
Returns empty strings on failure so the manifest is still valid.
"""
def _get(field: str) -> str:
try:
return run(["osmium", "fileinfo", "-g", f"header.option.{field}", str(pbf_path)])
except subprocess.CalledProcessError:
return ""
return {
"osm_replication_timestamp": _get("osmosis_replication_timestamp"),
"osm_replication_sequence_number": _get("osmosis_replication_sequence_number"),
}
def tag_and_release(
region: str,
source_pbf: str,
boundary_geojson: str,
version: str,
output_dir: str = "./releases",
) -> None:
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
target_pbf = out / f"{region}-{version}.osm.pbf"
meta_path = out / f"{region}-{version}.json"
sha_path = out / f"{region}-{version}.sha256"
source = Path(source_pbf)
if not source.is_file():
raise FileNotFoundError(f"Source PBF not found: {source_pbf}")
if not Path(boundary_geojson).is_file():
raise FileNotFoundError(f"Boundary GeoJSON not found: {boundary_geojson}")
# --- Step 2: extract regional snapshot ---------------------------------
if not target_pbf.exists():
print(f"Extracting {region} β {target_pbf.name} β¦")
run([
"osmium", "extract",
"--polygon", boundary_geojson,
"--strategy=complete-ways",
"--output", str(target_pbf),
str(source),
])
else:
print(f"Using existing extract: {target_pbf.name}")
# --- Step 3: VERSION.json manifest -------------------------------------
repl = osm_replication_meta(target_pbf)
manifest = {
"region": region,
"version": version,
"format": "osm.pbf",
"file": target_pbf.name,
"sha256": "", # filled in after checksum step
"extracted_at": datetime.now(timezone.utc).isoformat(),
"boundary": str(Path(boundary_geojson).name),
"osm_replication_timestamp": repl["osm_replication_timestamp"],
"osm_replication_sequence_number": repl["osm_replication_sequence_number"],
"generator": "osmium-tool",
"license": "ODbL 1.0",
}
# --- Step 4: SHA-256 checksum ------------------------------------------
print("Computing SHA-256 checksum β¦")
checksum = sha256_file(target_pbf)
manifest["sha256"] = checksum
sha_path.write_text(f"{checksum} {target_pbf.name}\n", encoding="utf-8")
meta_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
print(f"Manifest written: {meta_path.name}")
# --- Steps 5β6: Git tag + LFS push ------------------------------------
try:
run(["git", "add", str(meta_path), str(sha_path)])
run(["git", "commit", "-m", f"release: {region} {version}"])
run(["git", "tag", "-a", version,
"-m", f"OSM extract {region} {version} seq={repl['osm_replication_sequence_number']}"])
run(["git", "push", "origin", "main", "--follow-tags"])
print(f"Tagged and pushed: {version}")
except subprocess.CalledProcessError as exc:
print(f"Git operation failed:\n{exc.stderr}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) != 5:
print("Usage: python tag_release.py <region> <source.pbf> <boundary.geojson> <vX.Y.Z>")
sys.exit(1)
tag_and_release(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
Validation & Output Verification
After the pipeline completes, verify the release before promoting it to downstream consumers.
1. Checksum integrity
cd releases/
sha256sum -c nairobi-v1.4.2.sha256
# expected: nairobi-v1.4.2.osm.pbf: OK
2. Manifest freshness check
Parse VERSION.json to confirm the replication timestamp falls within your permitted staleness window (typically 24β72 hours for tile servers, shorter for routing engines):
import json
from datetime import datetime, timezone
manifest = json.loads(open("releases/nairobi-v1.4.2.json").read())
extracted = datetime.fromisoformat(manifest["extracted_at"])
age_hours = (datetime.now(timezone.utc) - extracted).total_seconds() / 3600
assert age_hours < 72, f"Extract is {age_hours:.1f} h old β exceeds freshness threshold"
print(f"Freshness OK: {age_hours:.1f} h old, seq {manifest['osm_replication_sequence_number']}")
3. Geometry and feature-count sanity check
Run ogrinfo on the PBF to confirm node and way counts are within expected bounds. A count drop of more than 5 % compared with the previous release warrants manual review before publication:
ogrinfo -al -so releases/nairobi-v1.4.2.osm.pbf lines
# inspect: Feature Count, Extent, Layer SRS
4. Git tag verification
Confirm the annotated tag is present on the remote and references the correct commit:
git ls-remote --tags origin | grep v1.4.2
git show v1.4.2 --stat
Failure Modes
-
osmium fileinforeturns empty replication fields β the source PBF was produced by a tool that does not write theosmosis_replication_*header options (e.g., a custom Overpass export). Populate the fields manually from the Overpass API response timestamp or the Geofabrik download page. -
SHA-256 mismatch on download β the artifact was re-uploaded or overwritten after tagging. Treat the tag as invalid, re-extract from the same source sequence, and re-tag with an incremented PATCH version.
-
git lfs pushfails with βbatch request too largeβ β the LFS server has a per-batch object-count limit. Push with--batch-size 1to send each object individually:git lfs push --all --batch-size 1 origin. -
Downstream pipeline loads wrong extract due to ambiguous naming β two regions share a common prefix (e.g.,
nairobiandnairobi-north). Enforce exact-match lookups against theregionfield insideVERSION.jsonrather than relying on filename prefix matching.
Related
- Release Tagging Strategies for Spatial Basemaps β parent guide covering the full spatial basemap release workflow, CRS validation, and multi-region tag coordination
- Feature Branching for GIS Development Teams β how to isolate extraction boundary changes and filter updates on feature branches before cutting a release
- Automated Conflict Detection in Merge Requests β CI gates that validate topology and geometry integrity before a release branch is merged
- Large File Handling in DVC for GIS β alternative to Git LFS for tracking
.osm.pbfartifacts with content-addressed storage and remote caching