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) | 0.8-2.3 s | 3.6-3.9 s | 0.2-0.6x (2-5x faster) |
| Invalid subset (1 poly, 2026-08-03) | 11.9 ms | 2.2 ms | 5.4x |
| Full dataset (1.58M polys) | 3.5-4.3 s | 3.3-3.5 s | 1.0-1.2x |
Structure batch on a production GIS dataset, read from the original
GeoPackage (its geometry blobs are WKB; 813 features flattened to
1,579,030 polygon parts). i5-12400F (6C/12T), release profile (LTO),
mimalloc (default feature), Rayon 12-thread batch on both sides. GEOS
is conda-forge libgeos 3.14.1, MSVC, serial per-call, no LTO; "par
batch" means many GEOS C calls run concurrently via Rayon. GEOS
geometries are built via CoordSeq direct construction, no WKT
round-trip; the one-time pre-build of 1.58M GEOS geometries (1.4-1.5 s)
is excluded from the timings.
| Dataset | GeoRepair (par) | GEOS (par batch) | vs GEOS |
|---|---|---|---|
| Validation (1.58M) | 0.8-2.3 s (0.5-1.5 µs/poly) | 3.6-3.9 s (2.3-2.5 µs/poly) | 0.2-0.6x |
| Invalid subset (1 poly, 2026-08-03) | 11.9 ms | 2.2 ms | 5.4x |
| Full dataset (1.58M polys) | 3.5-4.3 s | 3.3-3.5 s | 1.0-1.2x |
Two settled runs per source; the bands cover the run-to-run spread. The GeoRepair column is measured in the same process as the GEOS column (co-residency inflates it 5-15% vs a standalone run, where the full pass is ~4.1-4.2 s).
Structure strategy, same machine/profile, CoordSeq direct GEOS
construction. Both columns measured in the same process; the GeoRepair
column is the Rayon 12-thread batch (a standalone run measures 10-30%
lower). Ratio is GEOS / GeoRepair, i.e. how many times GeoRepair is
faster. Stars: *** >= 100x, ** 10-100x, * 1-10x, blank means GEOS
is faster.
Polygons:
| Benchmark | GeoRepair (µs) | GEOS (µs) | Ratio | ★ |
|---|---|---|---|---|
| valid polygon 4v | 0.055 | 0.263 | 4.78x | * |
| valid polygon 10v | 0.168 | 0.324 | 1.93x | * |
| valid polygon 50v | 0.413 | 0.443 | 1.07x | * |
| valid polygon 100v | 0.624 | 0.576 | 0.92x | |
| valid polygon 500v | 2.362 | 1.581 | 0.67x | |
| valid polygon 1000v | 5.204 | 2.556 | 0.49x | |
| valid polygon 5000v | 38.76 | 11.27 | 0.29x | |
| valid polygon 10000v | 57.32 | 19.04 | 0.33x | |
| invalid bowtie 4v | 0.708 | 22.70 | 32.1x | ** |
| invalid star 100v | 9.035 | 8.155 | 0.90x | |
| self-touch poly | 0.869 | 19.78 | 22.8x | ** |
| collapsed poly | 0.495 | 15.86 | 32.0x | ** |
| near-collinear | 1.247 | 18.52 | 14.8x | ** |
| large coord 1e12 | 0.081 | 0.260 | 3.2x | * |
LineStrings & MultiLineStrings:
| Benchmark | GeoRepair (µs) | GEOS (µs) | Ratio | ★ |
|---|---|---|---|---|
| valid line | 0.011 | 0.053 | 4.8x | * |
| zero-length line | 0.008 | 0.412 | 51.5x | ** |
| valid ls 4v | 0.032 | 0.111 | 3.5x | * |
| valid ls 10v | 0.078 | 0.088 | 1.1x | * |
| valid ls 50v | 0.107 | 0.082 | 0.8x | |
| valid ls 100v | 0.235 | 0.125 | 0.5x | |
| valid ls 500v | 0.760 | 0.588 | 0.8x | |
| collinear ls 4v | 0.027 | 0.042 | 1.6x | * |
| collinear ls 10v | 0.057 | 0.052 | 0.9x | |
| collinear ls 50v | 0.121 | 0.087 | 0.7x | |
| collinear ls 100v | 0.208 | 0.133 | 0.6x | |
| collinear ls 500v | 0.607 | 0.604 | 1.0x | * |
| zigzag ls 10v | 0.041 | 0.046 | 1.1x | * |
| zigzag ls 50v | 0.110 | 0.092 | 0.8x | |
| zigzag ls 100v | 0.248 | 0.123 | 0.5x | |
| zigzag ls 500v | 0.576 | 0.473 | 0.8x | |
| spiral ls 10v | 0.044 | 0.075 | 1.7x | * |
| spiral ls 50v | 0.123 | 0.099 | 0.8x | |
| spiral ls 100v | 0.230 | 0.142 | 0.6x | |
| self-int ls 5v | 0.031 | 0.232 | 7.5x | * |
| dense self ls 10v | 0.043 | 0.203 | 4.7x | * |
| dense self ls 50v | 0.117 | 0.158 | 1.4x | * |
| dense self ls 100v | 0.239 | 0.155 | 0.6x | |
| duped ls 100v | 0.492 | 0.356 | 0.7x | |
| mls 50x3v | 1.671 | 0.468 | 0.3x | |
| mls 50x4v | 1.610 | 0.580 | 0.4x |
Special shapes:
| Benchmark | GeoRepair (µs) | GEOS (µs) | Ratio | ★ |
|---|---|---|---|---|
| star-burst 10sp | 0.058 | 0.126 | 2.2x | * |
| star-burst 50sp | 0.196 | 0.169 | 0.9x | |
| star-burst 100sp | 0.258 | 0.192 | 0.7x | |
| star-burst 500sp | 1.063 | 0.613 | 0.6x | |
| collinear ov 10seg | 0.073 | 0.173 | 2.4x | * |
| collinear ov 50seg | 0.225 | 0.242 | 1.1x | * |
| collinear ov 100seg | 0.440 | 0.302 | 0.7x | |
| collinear ov 500seg | 1.556 | 1.008 | 0.6x | |
| x-scale 10v | 0.035 | 0.075 | 2.1x | * |
| x-scale 50v | 0.123 | 0.112 | 0.9x | |
| x-scale 100v | 0.375 | 0.219 | 0.6x | |
| ringing 100v | 0.199 | 0.095 | 0.5x | |
| ringing 500v | 1.014 | 0.376 | 0.4x | |
| hilbert 256v | 0.272 | 0.370 | 1.4x | * |
| hilbert 1024v | 1.210 | 1.281 | 1.1x | * |
| lissajous 200v | 0.295 | 0.295 | 1.0x | * |
| lissajous 500v | 0.602 | 0.484 | 0.8x | |
| lissajous 1000v | 2.313 | 2.355 | 1.0x | * |
| lissajous 7:4 500v | 0.470 | 0.660 | 1.4x | * |
| spoke 10sp | 0.056 | 0.086 | 1.5x | * |
| spoke 50sp | 0.147 | 0.127 | 0.9x | |
| spoke 100sp | 0.222 | 0.171 | 0.8x | |
| spoke 500sp | 1.647 | 0.980 | 0.6x | |
| star-comb 20sp | 0.063 | 0.095 | 1.5x | * |
| star-comb 100sp | 0.121 | 0.174 | 1.4x | * |
| star-comb 500sp | 0.548 | 0.683 | 1.2x | * |
Holes, overlaps & grids:
| Benchmark | GeoRepair (µs) | GEOS (µs) | Ratio | ★ |
|---|---|---|---|---|
| hole hier 5h | 1.016 | 1.143 | 1.1x | * |
| hole hier 20h | 5.994 | 8.025 | 1.3x | * |
| hole hier 50h | 47.57 | 31.20 | 0.66x | |
| overlap mp 5sh | 2.128 | 29.90 | 14.0x | ** |
| overlap mp 20sh | 5.411 | 454.3 | 84.0x | ** |
| overlap mp 50sh | 31.17 | 7330.2 | 235x | *** |
| dense grid 5x5=25 | 6.870 | 1785.7 | 260x | *** |
| dense grid 10x10=100 | 44.08 | 14367 | 326x | *** |
| dense grid 20x20=400 | 757.3 | 103750.3 | 137x | *** |
| sliver 100v | 1.765 | 7.294 | 4.1x | * |
| sliver 500v | 14.93 | 18.88 | 1.26x | * |
Arrange pipeline (CDT fallback):
| Benchmark | GeoRepair (µs) | GEOS (µs) | Ratio | ★ |
|---|---|---|---|---|
| arrange valid 4v | 0.046 | 0.263 | 5.7x | * |
| arrange valid 10v | 0.135 | 0.324 | 2.4x | * |
| arrange valid 50v | 0.433 | 0.443 | 1.0x | * |
| arrange bowtie 4v | 0.928 | 22.70 | 24.5x | ** |
| arrange star 10sp | 0.068 | 0.126 | 1.9x | * |
| arrange star 50sp | 0.244 | 0.169 | 0.7x |
Pattern: GeoRepair wins big on invalid repair and MultiPolygon unification (GEOS hits quadratic worst cases: dense grids 137-326x, overlaps 14-235x, bowtie/collapsed/self-touch 15-32x). GEOS wins on large valid polygons (0.3-0.7x on 500-10000 verts) where mature C++ optimization dominates. Sub-µs rows fluctuate with Rayon dispatch overhead and should not be read as precise.
# Real-world dataset benchmark with GEOS comparison (system GEOS)
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,io-shp --bench real_world
# Real-world dataset, no GEOS (load + validation + repair + output gate)
BENCH_FILE=benches/real_world/data_0.bin ./target/release/deps/real_world-*.exe --fast
# Synthetic benchmarks with GEOS comparison
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
# Synthetic benchmarks, serial + parallel columns only (no GEOS)
cargo bench --features arrange,structure,parallel,simd --bench benchFull GEOS setup lives in benches/AGENTS.md. Measurement rules that
matter: always take the settled second run (first-run-after-build is
inflated ~18% by Windows Defender + cold LTO code); never trust a bench
binary you cannot trace to a source file; examples/sp_diag.rs is the
invalid-subset diagnostic (acceptance counts + per-stage timing on the
biggest giant).
- 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.
- Validator strictness gate (deliberate policy, real-world impact ~1 poly).
Our validator runs Shewchuk exact predicates (agreeing with GEOS on the
OGC definition, 934/934 GEOS XML suite pass) plus one relative noise
gate: edges whose exact orientation is nonzero but within ~32 ulps of
the pair's own length scale are treated as coincident (see
src/validation/mod.rs). Measured on the 1.58M real-world dataset (2026-08-03): 1,579,029 parts are winding-only (CW, GEOS-valid), and exactly 1 part carries a non-winding defect. Earlier counts (2,298 viaarrange::validate_polygon, 1,855 via the full validator) were inflated by a real bug: the product-form proper-crossing testo1 * o2 < 0.0treated a -0.0 orientation (an exact collinear touch, common on snapped real-world vertices) as a crossing, flagging GEOS-valid geometry. Fixed 2026-08-03 with a zero-safe strict opposite-sign predicate inedges_intersect_generaland in the sweep (segments_properly_cross,has_no_intersections), which also closed the--fastbench gate artifact:arrange::validate_polygonpreviously flagged all 2,298 real-world Structure outputs while the full validator and GEOS accepted them; it now agrees with GEOS (the zero-orient fix plus an inclusive hole-containment check, GEOS-aligned for boundary-touching holes). Repair contract is unchanged: a repair ships only validator-clean geometry and degrades to an empty GeometryCollection otherwise. Earlier 2026-08-03 differential-fuzz fixes still in force: exact-collinear micro-edge overlaps below the length gate, -0.0 pinches, closing-edge backtracking (the pair (0, n-1) was skipped outright), segment-local vertex-on-edge tolerance (a pair-max eps inflated past micro segments in mixed-magnitude rings). - W/12 pool-saturation floor. The parallel batch fills all 12 workers with giants; nested intra-poly rayon finds no idle threads in-batch. Standalone, the parallel check measures 96 → 53 ms; in the batch the per-giant serial chain (~370 ms for the biggest) is Amdahl-bound.
- Giants (>4,096 ring edges) route to the boolean pipeline.
Single-pass noding costs 168 ms on a 200k-edge giant vs 36 ms for
the boolean path;
SP_MAX_EDGES = 4096insrc/core/mod.rsis the gate. - Mass-overlap repairs are the slowest synthetic class (dense grid 20x20: ~0.76 ms/poly parallel) but remain 100x+ faster than GEOS on the same shapes.
- Python bindings:
tests/test_python.pycovers the WKT surface (18 tests). GeoJSON bindings were removed by design; the test file now imports onlyrepair_wkt/repair_wkt_batch/is_valid_wkt/validate_wkt/validate_wkt_batch/validate_and_fix_wkt. simd-portableis 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 is intentionally absent;-C target-cpu=nativeregresses the full pass ~25%. The one AVX2 kernel kept is the bbox scan, runtime-dispatched viais_x86_feature_detected!, bit-exact vs scalar.projrequires native PROJ;io-gpkg+projare mutually exclusive (sqlite3 link conflict).- GEOS XML suite coverage: 934/934 dispatched cases pass; 1,565
overlay/relate cases are skipped (overlay operations are out of
scope); 209 masked divergences (documented tolerance gates, e.g.
even-odd area 1e-6 for island-in-hole). The suite's WKT/WKB readers
are our own: external
wkt/wkbcrates are disallowed because they measure slower than the built-in readers. - Sub-µs synthetic rows are noise (Rayon dispatch overhead). The trustworthy metrics are the real-world batch numbers and the larger synthetic rows.
geo-repair is built on geo types and plugs into the georust ecosystem two
ways:
-
geo-traits sources (
geo-traitsfeature). Theinteropmodule runs validation and repair overgeo_traits::GeometryTrait/geo_traits::GeometryCollectionTrait, the trait layer implemented bygeo, geoarrow, geozero, andwkb. Any such source can be validated or repaired in one call (interop::is_valid_geometry,interop::make_valid_geometry,interop::make_valid_geometries, ...) without materializinggeotypes; results come back asgeo::Geometry<f64>. -
geo's
Validationtrait (always available).GeoRepairValidation(&geometry)wraps any&geo::Geometry<f64>and exposes geo_repair's validator through geo'sValidationtrait (.is_valid(),.check_validation(),.validation_errors()) with geo'sInvalid*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());| 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 |
Runtime-dispatched AVX2 bbox kernel (the one SIMD kernel that beat auto-vectorized scalar, 4.5x); all other kernels auto-vectorized scalar | yes |
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 (not WASM) | no |
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 |
Apache-2.0