README.md

July 16, 2026 · View on GitHub

pbfhogg
Fast OpenStreetMap PBF reader and writer for Rust

crates.io docs.rs MSRV 1.96 License

Rust library and CLI for reading, writing, and transforming OpenStreetMap PBF files. The full planet (87 GB, 11.6B elements) processes on a 32 GB machine - every command, bounded memory, no compromises.

Developed on Linux, untested elsewhere.

Built with LLMs. See LLM.md.

Features

  • Read .osm.pbf files sequentially, in parallel (par_map_reduce), or with a 3-stage pipelined decoder
  • Write valid .osm.pbf files with HeaderBuilder, BlockBuilder, and PbfWriter - dense node packing, delta encoding, configurable compression (none, zlib, zstd)
  • Blob passthrough for copying unmodified blobs during merge/cat - kernel-space copy eliminates userspace buffer overhead
  • Blob indexdata - embeds element type + ID range + spatial bbox in BlobHeader for O(1) blob classification without decompression
  • Blob tag index - per-blob tag key metadata enables skipping decompression of blobs that provably lack required tag keys
  • O_DIRECT I/O - optional linux-direct-io feature bypasses the page cache for planet-scale reads and writes
  • io_uring writes - optional linux-io-uring feature for maximum throughput when I/O-bound

Planet scale

Every command listed below runs on the full planet on normal hardware. Measured on an AMD Ryzen 9 5900X, 32 GB DDR4 RAM, NVMe SSD, with ~28 GB available RAM. Input: 87 GB indexed planet PBF, 11.6B elements.

CommandWallPeak anon RSS
add-locations-to-ways --index-type external9m52s12.0 GB
apply-changes (OSC-only daily diff, zstd:1)4m29s2.4 GB
apply-changes --locations-on-ways (daily diff)2m15s~3.3 GB
build-geocode-index5m47s~25 GB
cat (indexdata generation)1m27s~200 MB
cat --clean version5m34s750 MB
cat --dedupe2h13m1.4 GB
cat --type way (raw passthrough)45s10 MB
check --ids (streaming, default)57s504 MB
check --ids --full1m10s2.22 GB
check --refs1m0s2.17 GB
diff -j 16 (two independent 47-day-apart planets, text)3m48s586 MB
diff --format osc -j 16 (two independent 47-day-apart planets)4m54s634 MB
extract --complete (Europe bbox)3m42s4.7 GB
extract --simple (Europe bbox)3m42s3.0 GB
extract --smart (Europe bbox)4m28s11.17 GB
getid6.1s27 MB
getid --invert1m31s102 MB
getparents22s506 MB
inspect6.5s5 MB
inspect --extended13m41s34 MB
inspect --nodes -j 1656.8s410 MB
inspect --tags -j 162m50s17.5 GB
merge-changes --osc-seq N (1-OSC daily)44s2 MB
merge-changes --osc-range A..B (7-OSC, ~1 week of dailies)54s2 MB
multi-extract --simple -c (5 regions, Europe bbox)14m44s9.4 GB
multi-extract --smart -c (5 regions, Europe bbox)13m58s22.9 GB
renumber3m11s3.3 GB
sort (already-sorted input)2m27s476 MB
tags-filter (default two-pass, w/highway=primary)1m55s2.6 GB
tags-filter -i w/highway=primary (invert-match)7m57s7.0 GB
tags-filter -R highway=primary52s688 MB
time-filter (snapshot, cutoff 2024-01-01)4m24s812 MB

Three commands write temp files to the output's parent directory: add-locations-to-ways --index-type external (~246 GB), diff (parallel by default; ~30 GB text shards at planet), diff --format osc (~45 GB XML shards). The others are scratch-free. diff -j 1 restores the scratch-free sequential path if temp disk is scarce.

History-shaped input (per-element version history and visibility, consumed by time-filter on multi-version files) is functional but deliberately outside this validation surface: no history dataset is configured or benchmarked, and the table above says nothing about it.

Per-command phase breakdowns are in reference/performance.md; per-command optimization arcs and retired phase breakdowns at older architectures are in reference/performance-history.md. Note that recorded results always track the latest git head and may not match the released version.

The goal for pbfhogg 1.0 is that every CLI command must be planet-scale safe on a 32GB RAM host (28-ish free GB.)

Usage

[dependencies]
pbfhogg = "0.5"
use pbfhogg::{ElementReader, Element};

let reader = ElementReader::from_path("input.osm.pbf")?;

if reader.header().is_sorted() {
    println!("PBF is sorted by type then ID");
}

reader.for_each(|element| {
    if let Element::Way(way) = element {
        // process way
    }
})?;
# Ok::<(), std::io::Error>(())

Writing

use pbfhogg::write::block_builder::{HeaderBuilder, BlockBuilder};
use pbfhogg::write::writer::{PbfWriter, Compression};

let header_bytes = HeaderBuilder::new()
    .bbox(9.0, 54.0, 13.0, 58.0)
    .sorted()
    .build()?;
let mut writer = PbfWriter::to_path("output.osm.pbf".as_ref(), Compression::default(), &header_bytes)?;

let mut bb = BlockBuilder::new();
bb.add_node(1, 556_761_000, 125_683_000, [("name", "Copenhagen")], None);
if let Some(bytes) = bb.take()? {
    writer.write_primitive_block(bytes)?;
}
writer.flush()?;
# Ok::<(), std::io::Error>(())

Read modes

MethodOrderUse case
for_eachFile orderSequential processing, order-dependent consumers
for_each_pipelinedFile orderFastest ordered read (parallel decompression)
for_each_block_pipelinedFile orderConsumers that need owned PrimitiveBlock for parallel processing
into_blocks_pipelinedFile orderLoop control: early exit, zipping two files
par_map_reduceArbitraryAggregation where order doesn't matter

CLI

pbfhogg inspect <file>                    File inspection (blocks, ordering, counts)
pbfhogg inspect --indexed <file>          Check if PBF has indexdata (exit code 0/1)
pbfhogg inspect tags <file>               Tag key=value frequencies
pbfhogg check <file> --ids                Validate ID uniqueness and ordering
pbfhogg check <file> --refs               Validate referential integrity
pbfhogg cat <files...> -o <out>           Concatenate PBFs (-t node,way,relation to filter)
pbfhogg sort <file> -o <out>              Sort into standard order (nodes, ways, relations by ID)
pbfhogg repack <file> -o <out>            Re-encode at a configurable --elements-per-blob N cap
pbfhogg degrade <file> -o <out>           Adversarial PBF generator
pbfhogg renumber <file> -o <out>          Renumber all IDs sequentially, remap cross-references
pbfhogg extract <file> -o <out> -b <bbox> Extract by bounding box
pbfhogg extract <file> -o <out> -p <geo>  Extract by GeoJSON polygon
pbfhogg extract <file> -c <config>        Multi-extract from JSON config
pbfhogg add-locations-to-ways <f> -o <o>  Embed node coordinates in ways
pbfhogg apply-changes <base> <osc> -o <o> Apply OSC diff (--locations-on-ways)
pbfhogg merge-changes <oscs...> -o <out>  Merge multiple OSC files (--simplify)
pbfhogg diff <old> <new>                  Compare two PBFs (-v verbose, --format osc)
pbfhogg tags-filter <file> -o <out> <exp> Filter by tag expressions (PBF or OSC input)
pbfhogg getid <file> -o <out> <ids>       Extract elements by ID (--invert to remove)
pbfhogg getparents <file> -o <out> <ids>  Find ways/relations referencing given IDs
pbfhogg time-filter <file> -o <out> <ts>  Filter history PBF to a timestamp
pbfhogg build-geocode-index <f> -d <dir>  Build reverse geocoding index

All write commands accept --compression (none, zlib, zstd, or with level: zlib:9). Default is zlib:6 for osmium interop. For internal pipelines that don't need osmium/JOSM compatibility, zstd:1 can be a substantial wall-time win where output compression is what limits the pipeline - measured ≈ −14 % on Europe add-locations-to-ways --index-type external (270.8 s zlib:6 at 0dc8ae1 → 233.3 s zstd:1 at 4fc8e35, UUID e2fba1bf) by relieving consumer/compression saturation in stage 4, at similar output size. This does not generalise by dataset size: the same command at planet scale is not compression-bound, and zstd:1 buys ≤5 % of the streaming phase there for ~80 % less compression CPU, at ~4 % larger output (2026-07-14, UUIDs 0e9d93cc vs ed5dd6c5). Measure your own pipeline before assuming the Europe number transfers. Commands that benefit from indexdata will error without it - pass --force to proceed (slower), or generate indexed PBFs with pbfhogg cat input.osm.pbf -o indexed.osm.pbf.

Applying accumulated dailies: when several OSC diffs have piled up (say a week of dailies), squash them with merge-changes first and run apply-changes once on the result, instead of applying each diff in turn:

pbfhogg merge-changes day1.osc.gz day2.osc.gz ... day7.osc.gz -o week.osc.gz
pbfhogg apply-changes planet.osm.pbf week.osc.gz -o updated.osm.pbf

One merge plus one apply is much cheaper than N applies: each apply-changes pass rewrites the whole base PBF, while the squash itself is cheap - 55 s at planet scale for 7 dailies (see the table above; down from 4m27s since the parallel merge drain, commit 99057fa). Adding --simplify keeps only the last change per object across the merged diffs, so the single apply can also touch fewer elements than the individual applies would in sequence.

See docs/cli/commands.md for detailed command documentation, docs/guide/advanced.md for O_DIRECT, io_uring, and index type details.

Performance

Read throughput - 59M elements in Denmark (461 MB), best of 3 (commit 90df51f):

ToolModeTime
pbfhoggparallel0.31s
osmpbf 0.3parallel0.53s
pbfhoggpipelined1.3s
Planetiler 0.10parallel2.0s
pbfhoggsequential2.8s
osmpbf 0.3sequential5.6s
osmium 1.19cat → opl5.7s

CLI commands vs osmium - Denmark (487 MB, commit 6fc1283):

Commandpbfhoggosmiumspeedup
sort (sorted, indexdata)0.7s11.6s17x
apply-changes (indexdata)0.6s7.2s12x
cat --type way (raw passthrough)0.24s2.22s9.3x
extract --smart (Tokyo bbox, Japan)4.7s13.4s2.9x
tags-filter highway=primary -R0.2s0.56s2.8x
add-locations-to-ways9.7s12.1s1.2x

All CLI commands are cross-validated against osmium on Denmark (brokkr verify). See reference/osmium-parity.md for the full comparison matrix, DEVIATIONS.md for intentional behavioral differences, and CORRECTNESS.md for parser/encoder edge cases. Detailed per-command benchmarks and phase breakdowns are in reference/performance.md.

System: AMD Ryzen 9 5900X (12c/24t), 32 GB DDR4, NVMe SSD, Linux 6.18. Measured with brokkr bench.

Acknowledgements

pbfhogg started as a fork of osmpbf by Thomas Bruggemann. osmium-tool and libosmium by Jochen Topf are the reference implementation - pbfhogg's CLI covers the same use cases, cross-validated against osmium using brokkr.

License

Licensed under either of

at your option.