Skip to content

GeoRepair

OGC geometry repair and validation for Rust. Passes the GEOS XML validation suite.

crate docs MSRV License Status Ko-fi

This crate is experimental. The API is actively evolving: expect breaking changes between 0.x releases. Core algorithms, I/O backends, and feature flags are all subject to change as we improve correctness and performance.

Detects and fixes invalid GIS geometries (self-intersections, unclosed rings, degenerate shapes, NaN coordinates) using algorithms selected by geometry type. Built-in I/O for WKB, WKT, and a custom binary batch format with no extra dependencies.

The Structure strategy (default) mirrors GEOS ST_MakeValid: planar graph extraction, face walking, winding-number assembly. The Arrange strategy is a CDT-based fallback for complex topologies.

Performance on the 1,579,030-polygon production dataset (i5-12400F, release, parallel batch, GEOS 3.14.1 conda-forge as reference):

Dataset GeoRepair GEOS vs GEOS
Validation (1.58M) 3.0-3.2 s 3.6-3.9 s 0.75-0.9x
Invalid subset 0 polys (2026-08-06) 0 -
Full dataset (1.58M polys) 3.8-4.0 s 3.3-3.5 s 1.1-1.2x

Performance

Real-world dataset (1,579,030 polygons)

Structure batch on a production GIS dataset (813 features from a GeoPackage, flattened to 1,579,030 polygon parts). i5-12400F (6C/12T), release LTO, Rayon 12-thread batch; GEOS is conda-forge 3.14.1, serial per-call, run concurrently via Rayon, geometries built via CoordSeq direct construction (the one-time 1.5 s pre-build is excluded). Ratio is GEOS / GeoRepair: >1 means GeoRepair is faster (same convention as the synthetic tables).

Dataset GeoRepair GEOS Ratio
Validation (1.58M) 2.1 s (1.3 µs/poly) 3.0 s (1.9 µs/poly) 1.4x
Full pass (1.58M) 3.6 s (2.3 µs/poly) 3.0 s (1.9 µs/poly) 0.83x

Validity agreement with GEOS: 100% (full validator, 0/0 disagreements); the gate screen flags 139 of 1,579,030 that GEOS accepts (99.99%, 2026-09-18).

Synthetic benchmarks

Full table, parallel batch (µs); ratio = GEOS / GeoRepair, >1 means we win. Methodology: docs/BENCHMARKS.md. Regenerate: python scripts/readme_bench_table.py --update <bench.json>; CI gate: python scripts/readme_bench_table.py --check <bench.json>.

Benchmark GeoRepair GEOS Ratio
valid polygon 4v 0.049 0.235 4.8x
valid polygon 10v 0.149 0.322 2.2x
valid polygon 50v 0.37 0.515 1.4x
valid polygon 100v 0.714 0.614 0.86x
valid polygon 500v 2.3 1.9 0.81x
valid polygon 1000v 3.9 2.2 0.57x
valid polygon 5000v 22.0 9.4 0.43x
valid polygon 10000v 47.7 17.7 0.37x
invalid bowtie 4v 0.68 17.2 25x
invalid bowtie 50v 6.6 63.5 9.6x
invalid bowtie 100v 23.3 112 4.8x
invalid bowtie 500v 60.1 465 7.7x
invalid bowtie 1000v 115 913 8.0x
star poly 100v 7.7 6.4 0.83x
star poly 500v 164 81.0 0.49x
star poly 1000v 236 269 1.1x
spaghetti 500v 4457 2494 0.56x
spaghetti 2000v 24940 16540 0.66x
self-touch 100v 0.69 0.714 1.0x
self-touch 500v 2.2 1.6 0.71x
self-touch 1000v 3.9 3.5 0.90x
collapsed 100v 20.1 117 5.9x
collapsed 500v 90.8 487 5.4x
collapsed 1000v 166 916 5.5x
near-collinear 100v 61.4 180 2.9x
near-collinear 500v 814 1115 1.4x
near-collinear 1000v 2544 2627 1.0x
large coord 1e12 100v 0.695 0.44 0.63x
large coord 1e12 500v 8.5 1.2 0.14x
large coord 1e12 1000v 30.0 2.5 0.08x
valid line 0.009 0.038 4.3x
zero-length line 0.006 0.267 45x
valid ls 4v 0.028 0.038 1.4x
valid ls 10v 0.059 0.042 0.72x
valid ls 50v 0.177 0.083 0.47x
valid ls 100v 0.337 0.118 0.35x
valid ls 500v 1.2 0.61 0.52x
valid ls 1000v 2.4 2.6 1.1x
collinear ls 4v 0.028 0.538 20x
collinear ls 10v 0.068 0.578 8.5x
collinear ls 50v 0.283 0.632 2.2x
collinear ls 100v 0.415 0.886 2.1x
collinear ls 500v 2.4 1.9 0.79x
collinear ls 1000v 3.7 20.0 5.4x
zigzag ls 10v 0.071 1.3 19x
zigzag ls 50v 0.216 5.0 23x
zigzag ls 100v 0.268 9.7 36x
zigzag ls 500v 1.4 61.2 43x
zigzag ls 1000v 2.8 120 43x
spiral ls 10v 0.058 0.886 15x
spiral ls 50v 0.553 5.1 9.3x
spiral ls 100v 1.5 18.1 12x
spiral ls 500v 127 563 4.4x
spiral ls 1000v 362 1933 5.3x
self-int ls 100v 6.3 2.3 0.36x
self-int ls 500v 43.5 3.3 0.08x
self-int ls 1000v 121 6.4 0.05x
dense self ls 10v 0.074 1.3 17x
dense self ls 50v 0.231 5.2 22x
dense self ls 100v 0.288 12.5 43x
dense self ls 500v 1.3 129 102x
dense self ls 1000v 2.9 341 120x
duped ls 100v 0.365 0.715 2.0x
duped ls 500v 1.1 1.4 1.3x
duped ls 1000v 2.2 4.2 1.9x
mls 50x3v 2.8 30.8 11x
mls 250x3v 15.5 129 8.3x
mls 500x3v 39.6 276 7.0x
self-int mls 50x4v 31.0 78.1 2.5x
self-int mls 250x4v 193 521 2.7x
self-int mls 500x4v 619 1206 1.9x
star-burst 10sp 3.0 18.1 5.9x
star-burst 50sp 1.0 349 334x
star-burst 100sp 1.6 1438 873x
star-burst 500sp 8.5 47694 5632x
star-burst 1000sp 18.0 226317 12573x
collinear ov 10seg 1.1 13.2 12x
collinear ov 50seg 7.1 60.9 8.5x
collinear ov 100seg 14.3 119 8.3x
collinear ov 500seg 62.8 592 9.4x
collinear ov 1000seg 148 1284 8.7x
x-scale 10v 1.9 21.3 11x
x-scale 50v 61.6 414 6.7x
x-scale 100v 263 1784 6.8x
x-scale 500v 18884 60152 3.2x
x-scale 1000v 2257 357687 159x
ringing 100v 10.8 62.9 5.8x
ringing 500v 56.5 348 6.2x
ringing 1000v 118 962 8.1x
hilbert 256v 34.0 168 4.9x
hilbert 1024v 291 1298 4.5x
lissajous 200v 19.2 144 7.5x
lissajous 500v 46.3 386 8.3x
lissajous 1000v 85.8 745 8.7x
lissajous 2000v 184 1672 9.1x
lissajous 5000v 547 5204 9.5x
lissajous 7:4 500v 33.8 46.8 1.4x
spoke 10sp 2.5 15.0 5.9x
spoke 50sp 0.853 273 320x
spoke 100sp 1.5 1161 795x
spoke 500sp 9.8 46434 4761x
spoke 1000sp 23.1 214932 9300x
star-comb 20sp 0.199 2.6 13x
star-comb 100sp 3.4 34.2 9.9x
star-comb 500sp 76.1 743 9.8x
star-comb 1000sp 703 2660 3.8x
hole hier 5h 1.6 2.0 1.2x
hole hier 20h 5.3 8.1 1.5x
hole hier 50h 19.7 25.3 1.3x
hole hier 100h 44.6 57.4 1.3x
overlap mp 5sh 2.2 404 186x
overlap mp 20sh 10.4 2469 238x
overlap mp 50sh 21.1 6693 318x
overlap mp 100sh 49.1 14405 293x
dense grid 5x5=25 9.0 1662 185x
dense grid 10x10=100 48.5 12137 250x
dense grid 20x20=400 461 100233 217x
dense grid 30x30=900 1925 352380 183x
sliver 100v 1.6 2.5 1.6x
sliver 500v 8.9 23.2 2.6x
sliver 1000v 18.8 30.4 1.6x
arrange valid 4v 0.077 0.244 3.2x
arrange valid 10v 0.1 0.332 3.3x
arrange valid 50v 0.752 0.459 0.61x
arrange valid 100v 1.1 0.947 0.88x
arrange valid 500v 6.5 1.6 0.25x
arrange valid 1000v 8.4 3.3 0.39x
arrange bowtie 4v 1.4 15.9 11x
arrange bowtie 100v 36.1 115 3.2x
arrange star 10sp 2.4 14.0 5.8x
arrange star 50sp 0.826 284 344x
arrange star 100sp 2.3 1162 497x
arrange star 500sp 7.7 42753 5560x

Run benchmarks

# Real-world + synthetic with GEOS comparison (system GEOS, conda)
GEOS_LIB_DIR='D:\Miniconda\Library\lib' GEOS_INCLUDE_DIR='D:\Miniconda\Library\include' \
GEOS_VERSION=3.14.1 cargo bench --features bench-geos-system,arrange,structure,parallel,simd --bench real_world
GEOS_LIB_DIR='D:\Miniconda\Library\lib' GEOS_INCLUDE_DIR='D:\Miniconda\Library\include' \
GEOS_VERSION=3.14.1 cargo bench --features bench-geos-system,arrange,structure,parallel,simd --bench bench

# CI regression gate (no GEOS; fixed subset vs benches/bench_baseline.json)
python scripts/bench_gate.py

Measurement rules: always take the settled second run (first-run-after-build is inflated ~18%); the real-world table carries the median of three settled runs; never trust a bench binary you cannot trace to a source file. Full GEOS setup: docs/GEOS-SETUP.md.

Limitations

  1. GEOS comparison is against conda-forge MSVC GEOS (serial per-call, no LTO, no mimalloc) - a static LLVM-built GEOS would improve the GEOS side of every table.
  2. Validator strictness is deliberate: eps-class predicates (1e-12 x scale tolerance, fast-FP-first with exact escalation). On the 1.58M real-world dataset it agrees with GEOS 0/0 (winding-agnostic); it diverges on borderline inputs (213 baselined XML cases, mostly the OGC winding contract - docs/BENCHMARKS.md). Repair ships only validator-clean geometry and degrades to an empty GeometryCollection otherwise.
  3. W/12 pool-saturation floor: the parallel batch fills all 12 workers with giants; nested intra-poly rayon finds no idle threads in-batch (Amdahl-bound; standalone the parallel check measures 96 ms vs 53 ms in-batch).
  4. Giants (>4,096 ring edges) route to the boolean pipeline - on a 200k-edge giant single-pass noding costs 168 ms vs 36 ms for the boolean path (SP_MAX_EDGES in src/core/mod.rs).
  5. Mass-overlap repairs are the slowest synthetic class (~0.46 ms/poly at dense grid 20x20) but stay ~230x faster than GEOS makeValid on the same shapes.
  6. Python bindings: tests/test_python.py covers the WKB and WKT surface (41 tests); GeoJSON bindings were removed deliberately.
  7. simd-portable is nightly-only (3 E0554 on stable, expected); hand-written AVX2 beyond the bbox scan measured slower than auto-vectorized scalar (point_in_ring 8.4x, is_ring_ccw 2.8x), and -C target-cpu=native regresses the full pass ~25%.
  8. proj requires native PROJ and is mutually exclusive with io-gpkg (sqlite3 link conflict); io-gpkg is a default feature, so proj users build with --no-default-features --features proj,....
  9. GEOS XML suite: 937/937 dispatched cases pass; 3,629 overlay/relate cases are skipped (out of scope); 213 masked divergences (documented tolerance gates). The suite's WKT/WKB readers are our own - external wkt/wkb crates are disallowed because they measure slower.
  10. Sub-µs synthetic rows are noise (Rayon dispatch overhead); the trustworthy metrics are the real-world batch numbers and the larger synthetic rows.

Integration with the geo ecosystem

geo-repair is built on geo types and plugs into the georust ecosystem two ways:

  • geo-traits sources (geo-traits feature). The interop module runs validation and repair over geo_traits::GeometryTrait / geo_traits::GeometryCollectionTrait, the trait layer implemented by geo, geoarrow, geozero, and wkb. Any such source can be validated or repaired in one call (interop::is_valid_geometry, interop::make_valid_geometry, interop::make_valid_geometries, ...) without materializing geo types; results come back as geo::Geometry<f64>.

  • geo's Validation trait (always available). GeoRepairValidation(&geometry) wraps any &geo::Geometry<f64> and exposes geo_repair's validator through geo's Validation trait (.is_valid(), .check_validation(), .validation_errors()) with geo's Invalid* error taxonomy. The orphan rule prevents implementing geo's trait for geo's own types, so the adapter is the bridge. Mapping is best-effort: geo_repair's stricter gates (32-ulp collinear, T-junction) surface through it, and classes geo does not model (ring closure, orientation, duplicates, ...) are omitted from the geo view.

use geo::algorithm::validation::Validation;
use geo_repair::GeoRepairValidation;

let adapter = GeoRepairValidation(&geometry);
assert!(!adapter.is_valid());

Python bindings

The geo-repair PyPI package exposes the full validation and repair surface over WKB bytes and WKT text, single geometry and batch (including a rayon-backed parallel batch). abi3 wheels (cp38-abi3) serve Python 3.8+; typing stubs are shipped in the wheel.

pip install geo-repair
import geo_repair

fixed = geo_repair.repair_wkt("POLYGON((0 0, 5 5, 5 0, 0 5, 0 0))")
assert geo_repair.is_valid_wkt(fixed)

was_valid, errors, fixed_wkb = geo_repair.validate_and_fix_wkb(wkb_bytes)
results = geo_repair.par_repair_wkb_batch(list_of_wkb_bytes)

Every function exists for WKB and WKT (repair_*, repair_*_batch, par_repair_*_batch, repair_validate_*, is_valid_*, validate_*, validate_and_fix_*, plus batch forms), with method (auto/arrange/structure) and keep_collapsed parameters. A QGIS Processing script (qgis/qgis_geo_repair.py) streams features through the WKB batch API. Full API reference: docs/BINDINGS.md.

C API

The ffi feature exposes a panic-safe C API over WKB and WKT, single geometries and parallel batches:

cargo build --release --features ffi
# output: target/release/geo_repair.{dll,so,dylib} + libgeo_repair.a + include/geo_repair.h
#include "geo_repair.h"

GeoRepairResult r = geo_repair_make_valid(bowtie_wkb, bowtie_wkb_len);
if (r.success) { /* r.wkb_data / r.wkb_len = fixed WKB */ }
geo_repair_free_result(&r);

Every result carries a GeoRepairErrorCode (None/Parse/InvalidInput/ InvalidGeometry/Encode/Panic); batches report per-item outcomes without failing as a whole. All results must be freed with the matching geo_repair_free_* (double-free safe). The ABI (struct layouts, error codes) is fixed from 0.14.2; panic containment requires the release profile's panic = "unwind". Prebuilt libraries for Windows, Linux, and macOS are attached to every GitHub release. Full API reference: docs/BINDINGS.md.

Features

Feature Description Default
arrange CDT-based polygon repair (requires spade) yes
structure Structure-based fast path repair yes
parallel Rayon parallel processing (non-WASM) yes
simd No-op on stable (kernels compiled unconditionally, LLVM auto-vectorized; hand-written AVX2 measured slower, kept only for the nightly simd-portable path) no
validate OGC validation predicates yes
mimalloc Use mimalloc global allocator yes
std Standard library + file I/O. Disable for no_std builds. yes
simd-portable Portable SIMD via core::simd (nightly only) no
memmap Memory-mapped binary file loading no
wasm WASM browser fetch (synchronous XHR) no
proj CRS transformation (placeholder) no
serde Geometry serde support (geo/serde) no
ffi C-compatible FFI bindings no
geo-traits Interop surface over geo_traits::GeometryTrait / GeometryCollectionTrait (geo, geoarrow, geozero, wkb sources) no
python Python bindings via PyO3 no
io-shp Shapefile format backend no
io-wkt No-op (WKT is built-in, kept for CI compatibility) no
io-csv CSV format backend no
io-gml GML/XML format backend no
io-gpkg GeoPackage format backend (default; gated out on wasm32) yes
io-all All opt-in backends except gpkg no
io-all-native All opt-in backends including gpkg no
bench-geos GEOS comparison benchmarks (build from source, MSVC, no LTO) no
bench-geos-system GEOS comparison benchmarks (link against system GEOS, conda-forge MSVC) no

License

Apache-2.0

About

Rust native OGC-compliant GIS parallel geometry repairing

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages