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.
GeoRepair repairs invalid OGC geometries and answers whether a geometry is valid. It fixes the common damage: self-intersections, unclosed rings, degenerate shapes, and NaN coordinates. Input that is already valid comes back unchanged.
Two strategies, chosen by geometry type:
- Structure (default) mirrors GEOS
ST_MakeValid: planar graph extraction, face walking, even-odd assembly. - Arrange is a CDT-based fallback for complex topologies.
WKB and WKT reading and writing, plus a custom binary batch format, are built in with no extra dependencies.
WKT keeps Z and M when you ask for it: read_wkt_zm returns the geometry, its
Dims and the extra ordinates in flat coordinate order (the same layout as
read_ewkb), and write_wkt_zm writes the modifier back. read_wkt reads the
same files and returns x/y, so a Z-enabled layer from OGR or QGIS loads without
error.
Everything is evaluated against a large test set ported from GEOS and CGAL, plus the crate's own fuzz tests. The result should always come out valid per GEOS, but the strategy differences can make the output slightly differ.
Structure batch on a GIS dataset (813 features from a GeoPackage, flattened to 1,579,030 polygon parts), measured 2026-09-18. 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).
| 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) | 2.1 s (1.3 µs/poly) | 3.2 s (2.1 µs/poly) | 1.5x |
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>. The overlap and
dense grid rows were re-measured 2026-11-05; the rest are the 2026-09-18 set.
| Benchmark | GeoRepair | GEOS | Ratio |
|---|---|---|---|
| valid polygon 4v | 0.051 | 0.237 | 4.7x |
| valid polygon 10v | 0.137 | 0.319 | 2.3x |
| valid polygon 50v | 0.245 | 0.415 | 1.7x |
| valid polygon 100v | 0.408 | 0.578 | 1.4x |
| valid polygon 500v | 1.4 | 1.7 | 1.2x |
| valid polygon 1000v | 2.4 | 2.7 | 1.1x |
| valid polygon 5000v | 11.1 | 8.4 | 0.76x |
| valid polygon 10000v | 21.4 | 17.4 | 0.81x |
| invalid bowtie 4v | 0.661 | 19.9 | 30x |
| invalid bowtie 50v | 7.7 | 65.8 | 8.5x |
| invalid bowtie 100v | 25.1 | 118 | 4.7x |
| invalid bowtie 500v | 56.9 | 511 | 9.0x |
| invalid bowtie 1000v | 109 | 1001 | 9.2x |
| star poly 100v | 8.6 | 6.6 | 0.76x |
| star poly 500v | 172 | 91.4 | 0.53x |
| star poly 1000v | 241 | 292 | 1.2x |
| spaghetti 500v | 1589 | 2633 | 1.7x |
| spaghetti 2000v | 11391 | 16251 | 1.4x |
| self-touch 100v | 0.493 | 0.759 | 1.5x |
| self-touch 500v | 1.5 | 1.6 | 1.0x |
| self-touch 1000v | 2.8 | 3.2 | 1.1x |
| collapsed 100v | 21.3 | 137 | 6.4x |
| collapsed 500v | 113 | 554 | 4.9x |
| collapsed 1000v | 189 | 1154 | 6.1x |
| near-collinear 100v | 73.1 | 191 | 2.6x |
| near-collinear 500v | 874 | 1184 | 1.4x |
| near-collinear 1000v | 2880 | 2857 | 0.99x |
| large coord 1e12 100v | 0.438 | 0.472 | 1.1x |
| large coord 1e12 500v | 1.2 | 1.1 | 0.91x |
| large coord 1e12 1000v | 2.6 | 2.6 | 1.0x |
| valid line | 0.009 | 0.039 | 4.1x |
| zero-length line | 0.007 | 0.247 | 36x |
| valid ls 4v | 0.022 | 0.039 | 1.7x |
| valid ls 10v | 0.037 | 0.044 | 1.2x |
| valid ls 50v | 0.078 | 0.095 | 1.2x |
| valid ls 100v | 0.146 | 0.126 | 0.86x |
| valid ls 500v | 0.406 | 0.495 | 1.2x |
| valid ls 1000v | 1.4 | 2.6 | 1.8x |
| collinear ls 4v | 0.022 | 0.563 | 26x |
| collinear ls 10v | 0.037 | 0.58 | 16x |
| collinear ls 50v | 0.067 | 0.72 | 11x |
| collinear ls 100v | 0.121 | 0.943 | 7.8x |
| collinear ls 500v | 0.477 | 2.0 | 4.1x |
| collinear ls 1000v | 1.3 | 8.6 | 6.5x |
| zigzag ls 10v | 0.042 | 1.3 | 31x |
| zigzag ls 50v | 0.074 | 4.7 | 63x |
| zigzag ls 100v | 0.126 | 10.0 | 79x |
| zigzag ls 500v | 0.493 | 56.0 | 113x |
| zigzag ls 1000v | 1.1 | 118 | 108x |
| spiral ls 10v | 0.096 | 0.835 | 8.7x |
| spiral ls 50v | 0.704 | 4.3 | 6.1x |
| spiral ls 100v | 1.6 | 16.1 | 10x |
| spiral ls 500v | 73.7 | 551 | 7.5x |
| spiral ls 1000v | 270 | 1861 | 6.9x |
| self-int ls 100v | 1.2 | 2.6 | 2.2x |
| self-int ls 500v | 5.1 | 3.8 | 0.75x |
| self-int ls 1000v | 9.1 | 6.5 | 0.72x |
| dense self ls 10v | 0.043 | 1.2 | 28x |
| dense self ls 50v | 0.094 | 5.3 | 57x |
| dense self ls 100v | 0.177 | 11.7 | 66x |
| dense self ls 500v | 0.671 | 122 | 181x |
| dense self ls 1000v | 1.1 | 349 | 305x |
| duped ls 100v | 0.268 | 0.759 | 2.8x |
| duped ls 500v | 0.492 | 1.4 | 2.8x |
| duped ls 1000v | 0.738 | 3.1 | 4.2x |
| mls 50x3v | 2.7 | 30.2 | 11x |
| mls 250x3v | 15.2 | 132 | 8.7x |
| mls 500x3v | 36.5 | 252 | 6.9x |
| self-int mls 50x4v | 17.6 | 77.5 | 4.4x |
| self-int mls 250x4v | 130 | 466 | 3.6x |
| self-int mls 500x4v | 402 | 1034 | 2.6x |
| star-burst 10sp | 3.0 | 14.6 | 4.8x |
| star-burst 50sp | 0.707 | 249 | 353x |
| star-burst 100sp | 1.3 | 1085 | 814x |
| star-burst 500sp | 7.6 | 36693 | 4838x |
| star-burst 1000sp | 14.2 | 188647 | 13304x |
| collinear ov 10seg | 1.1 | 13.1 | 12x |
| collinear ov 50seg | 7.4 | 60.5 | 8.2x |
| collinear ov 100seg | 13.5 | 117 | 8.7x |
| collinear ov 500seg | 62.2 | 628 | 10x |
| collinear ov 1000seg | 182 | 1426 | 7.8x |
| x-scale 10v | 2.4 | 21.1 | 8.8x |
| x-scale 50v | 63.4 | 436 | 6.9x |
| x-scale 100v | 306 | 1689 | 5.5x |
| x-scale 500v | 24135 | 59625 | 2.5x |
| x-scale 1000v | 1935 | 371599 | 192x |
| ringing 100v | 7.5 | 66.9 | 8.9x |
| ringing 500v | 27.9 | 364 | 13x |
| ringing 1000v | 65.6 | 991 | 15x |
| hilbert 256v | 35.5 | 171 | 4.8x |
| hilbert 1024v | 219 | 1344 | 6.1x |
| lissajous 200v | 37.0 | 174 | 4.7x |
| lissajous 500v | 58.6 | 419 | 7.2x |
| lissajous 1000v | 99.9 | 854 | 8.5x |
| lissajous 2000v | 230 | 1726 | 7.5x |
| lissajous 5000v | 671 | 6131 | 9.1x |
| lissajous 7:4 500v | 46.2 | 49.8 | 1.1x |
| spoke 10sp | 3.4 | 14.8 | 4.3x |
| spoke 50sp | 0.816 | 250 | 307x |
| spoke 100sp | 1.5 | 1098 | 739x |
| spoke 500sp | 10.7 | 36970 | 3442x |
| spoke 1000sp | 20.4 | 201201 | 9844x |
| star-comb 20sp | 0.386 | 2.7 | 7.0x |
| star-comb 100sp | 5.6 | 33.0 | 5.9x |
| star-comb 500sp | 78.4 | 679 | 8.7x |
| star-comb 1000sp | 668 | 2607 | 3.9x |
| hole hier 5h | 1.5 | 1.8 | 1.2x |
| hole hier 20h | 5.3 | 7.5 | 1.4x |
| hole hier 50h | 19.4 | 26.1 | 1.3x |
| hole hier 100h | 44.5 | 50.9 | 1.1x |
| overlap mp 5sh | 7.5 | 452 | 61x |
| overlap mp 20sh | 70.4 | 2727 | 39x |
| overlap mp 50sh | 451 | 6972 | 15x |
| overlap mp 100sh | 1484 | 15482 | 10x |
| dense grid 5x5=25 | 9.7 | 1751 | 180x |
| dense grid 10x10=100 | 45.8 | 13017 | 284x |
| dense grid 20x20=400 | 341 | 97618 | 286x |
| dense grid 30x30=900 | 996 | 396084 | 398x |
| sliver 100v | 1.3 | 2.5 | 1.9x |
| sliver 500v | 7.3 | 17.6 | 2.4x |
| sliver 1000v | 14.0 | 27.4 | 2.0x |
| arrange valid 4v | 0.059 | 0.275 | 4.6x |
| arrange valid 10v | 0.102 | 0.353 | 3.4x |
| arrange valid 50v | 0.426 | 0.484 | 1.1x |
| arrange valid 100v | 0.498 | 0.719 | 1.4x |
| arrange valid 500v | 1.9 | 2.0 | 1.1x |
| arrange valid 1000v | 3.5 | 3.6 | 1.0x |
| arrange bowtie 4v | 0.981 | 16.3 | 17x |
| arrange bowtie 100v | 23.1 | 116 | 5.0x |
| arrange star 10sp | 2.7 | 14.7 | 5.5x |
| arrange star 50sp | 1.0 | 261 | 250x |
| arrange star 100sp | 2.4 | 1189 | 491x |
| arrange star 500sp | 7.4 | 42434 | 5747x |
- 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 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. - 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).
- 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_EDGESinsrc/core/mod.rs). - Mass-overlap repairs are the slowest synthetic class (~1.5 ms for a 100-shell overlap) but still beat GEOS makeValid by 10x to 61x on the overlap rows and 180x to 398x on the dense grid.
- Python bindings:
tests/test_python.pycovers the WKB and WKT surface (41 tests); GeoJSON bindings were removed deliberately. 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-C target-cpu=nativeregresses the full pass ~25%.projrequires native PROJ and is mutually exclusive withio-gpkg(sqlite3 link conflict).io-gpkgis opt-in, so aprojbuild enablesprojand leavesio-gpkgoff.- WKT reads a dimension modifier, it does not infer one from the ordinate
count:
POINT Z (1 2 3)parses,POINT (1 2 3)stays an error, where GEOS infers Z from the third ordinate.
geo-repair plugs into georust two ways: a geo-traits interop module that
validates or repairs any GeometryTrait source (geo, geoarrow, geozero,
wkb) in one call, and a GeoRepairValidation adapter that exposes our
validator through geo's own Validation trait. Details and examples:
docs/INTEROP.md.
Three pieces exist for a host application: a batch entry that builds with or without the thread pool, an opt-in output policy, and error classes.
batch::{fix_polygon_batch, fix_polygon_batch_owned, fix_polygon_batch_chunked}run on rayon whenparallelis enabled, and on a serial loop when it is not.output::{OutputPolicy, apply_policy, make_valid_with_policy}reduce a result to one homogeneous geometry for a host that has no collection type.OutputReportcounts the parts kept and dropped.GeometryValidationError::code()gives a stable key,label()a short name, andclass()one of sixValidationClassvalues.ValidationResult::class_counts()totals them per class.
The default build carries no thread pool and no bundled SQLite. Enable
parallel and io-gpkg, or the full set, when the host wants them.
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). Install with
pip install geo-repair; abi3 wheels (cp38-abi3) serve Python 3.8+
and typing stubs ship in the wheel.
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. Mind that due to overhead it might suffer from performance issues.
Examples and full API reference: docs/BINDINGS.md.
The ffi feature exposes a panic-safe C API over WKB and WKT, single
geometries and parallel batches. Build with
cargo build --release --features ffi,parallel; the output is
target/release/geo_repair.{dll,so,dylib} plus libgeo_repair.a and
the include/geo_repair.h header.
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.
| Feature | Description | Default |
|---|---|---|
arrange |
CDT-based polygon repair (requires spade) |
yes |
structure |
Structure-based fast path repair | yes |
parallel |
Rayon parallel processing (non-WASM); the batch entries fall back to a serial loop without it |
no |
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 |
rstar |
R-tree index used by noding (enabled transitively by arrange/structure/validate) |
yes (transitive) |
mimalloc |
Use mimalloc global allocator | yes |
full |
The desktop set: default plus parallel and io-gpkg |
no |
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 via geo/proj (requires native PROJ) |
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-wkb |
No-op (WKB is built-in, kept for CI compatibility) | no |
io-geojson |
No-op (GeoJSON backend removed, kept for CI compatibility) | no |
io-csv |
CSV format backend | no |
io-gml |
GML/XML format backend | no |
io-gpkg |
GeoPackage format backend (gated out on wasm32) | no |
io-all |
All opt-in backends except gpkg | no |
io-all-native |
All opt-in backends including gpkg | no |
hotpath |
Dev-only: hotpath::measure instrumentation on hot paths |
no |
hotpath-alloc |
Dev-only: hotpath instrumentation plus allocation counters |
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 |
bench-gdal-system |
GDAL/OGR I/O comparison benchmark (link against system GDAL) | no |
Apache-2.0