Simplifying Geometries¶
gpio process simplify reduces geometry vertex counts with
coarsen — a multithreaded Rust
implementation of GEOS's simplification that produces byte-identical output
to GEOS 3.13.1 at a 9–14× speedup. Plain mode is a drop-in
shapely.simplify; --coverage mode is shapely.coverage_simplify: shared
edges between adjacent polygons stay shared, so a simplified admin or parcel
coverage stays gap-free and overlap-free.
The implementation lives in
core/process/simplify.py.
Installation¶
Simplification is an optional extra (the core install stays lean):
pip install 'geoparquet-io[simplify]'
# or, for a uv tool install:
uv tool install geoparquet-io --with coarsen
coarsen ships prebuilt wheels for Python 3.10+ on Linux, macOS and Windows.
It is LGPL-2.1 licensed and used as an unmodified dependency; without it,
gpio process simplify explains exactly this install step and everything
else in gpio works as before.
Basic Usage¶
# Tolerance is in the data's CRS units (here: 10 meters)
gpio process simplify parcels.parquet simplified.parquet --tolerance 10
# A polygonal coverage: keep shared edges shared, no gaps introduced
gpio process simplify admin.parquet simplified.parquet \
--tolerance 0.001 --coverage
import geoparquet_io as gpio
from geoparquet_io.api import ops
# Table API, chainable
gpio.read('parcels.parquet').simplify(10).write('simplified.parquet')
# Pure function over a PyArrow table
table = ops.simplify(my_arrow_table, tolerance=10, coverage=True)
Options¶
| Option | Default | Meaning |
|---|---|---|
--tolerance |
required | Douglas-Peucker tolerance, in CRS units |
--coverage |
off | Coverage mode: shared edges preserved across the whole file |
--preserve-topology |
on | Keep each geometry valid (plain mode only) |
--simplify-boundary |
on | Also simplify the coverage's outer boundary (--coverage only) |
--threads N |
auto | coarsen worker threads |
--geometry-column |
auto | Defaults to the file's primary geometry column |
--drop-empty |
off | Drop rows whose geometry is empty after simplification |
--simplify-crs |
— | Project to this CRS for the simplification (tolerance in its units), then back; auto-utm picks the UTM zone from the data |
--refresh-metrics |
off | Recompute vecorel metrics:area/metrics:perimeter from the simplified geometry |
Native GeoParquet 2.0 inputs are supported: the geometry arrives as a geoarrow WKB extension column, is simplified on its raw WKB, and the write preserves the 2.0 declaration (native geometry types and stats) — via the in-memory path, since the streaming writer covers plain-WKB 1.x outputs.
What happens to the metadata¶
Simplification changes the geometry, so the stats that describe it are recomputed rather than carried through stale:
- the
geoblock's per-columngeometry_typesandbboxare recomputed from the simplified data by the write funnel; - a declared bbox covering column is recomputed from the simplified geometries (float32 covering values are rounded outward, so the stored box always contains its geometry);
- CRS, edges and everything else carry through unchanged;
- columns derived from geometry are not touched by default — they would go
stale.
--refresh-metricsrecomputes the vecorelgpio add geometry-metricscolumns from the simplified shapes (Python API users compose:table.simplify(...)then.add_geometry_metrics()).
Geometries that collapse to empty at the given tolerance are kept (and
counted in a warning) by default; --drop-empty removes those rows instead
— empty geometries otherwise become NaN covering values and zero-area
features downstream. Null geometries are kept either way.
Scaling to planet-sized files¶
Plain mode streams: batches are read, simplified and written one row
group at a time, so peak memory is bounded by a single row group no matter
how large the file — a 40 GB, 100M+-row GeoParquet simplifies within a
laptop's RAM. (The streaming writer covers plain-WKB 1.x outputs; a 2.0
native output or a --row-group-size-mb byte target takes the in-memory
path.)
--coverage is different in kind: preserving shared edges requires seeing
every geometry in one pass — splitting a coverage into chunks measurably
introduces slivers of gap and overlap along the seam, small enough to slip
past downstream repair thresholds. So coverage mode holds the whole
geometry column in memory, and for global-scale coverages the right recipe
is to partition on a boundary where parcels do not share edges and
coverage-simplify each part:
gpio partition admin world_parcels.parquet parts/ # or h3 / kdtree / string
for f in parts/*.parquet; do
gpio process simplify "$f" "simplified/$(basename "$f")" --tolerance 5 --coverage
done
Tolerance units
--tolerance is in the data's CRS units, and global data in EPSG:4326
means degrees. For a metre tolerance pass --simplify-crs: the
geometries are projected to that CRS (or to the data's own UTM zone
with auto-utm), simplified there, and projected back — one pass, only
the geometry round-trips, attributes and the file's CRS stay untouched.
# 5 meter tolerance on lon/lat data, per-UTM-zone partitioned
gpio process simplify zone48.parquet out.parquet --tolerance 5 --simplify-crs auto-utm