Batch geometry cleaning for vector datasets. Point it at anything GeoPandas can read, and it runs an ordered, configurable pipeline of repair stages and writes a cleaned dataset plus a before/after validity report.
It is both a command-line tool and a library.
Vector data arrives broken. Polygons self-intersect, rings do not close, a supposedly single dataset turns out to be three different coordinate reference systems in a trench coat, overlay operations leave hairline slivers behind, and the same feature appears twice with its vertices in a different order.
The usual response is a notebook full of one-off fixes: a buffer(0) here, a
drop_duplicates there, a to_crs somewhere in the middle. That approach has
three problems. It is slow, because it runs per row. It is unrepeatable, because
the fixes live in a notebook rather than a config file. And it is silent — you
get a cleaned dataset with no record of what was wrong, what changed, or whether
it worked.
This tool takes the opposite position:
- Every operation is vectorized. Repairs run over whole numpy arrays through
the Shapely 2.x array API. There is no per-row
.apply()anywhere in the package, and a test enforces that. - Every run is described by a config file, so the same cleaning can be re-run, reviewed, and version-controlled.
- Every run produces a report — what was invalid and why, what each stage changed, how much area moved, and how long it took.
- The result can be verified, with a non-zero exit code when anything is still invalid, so a dirty dataset can break a build.
Python 3.11 or newer. All dependencies install cleanly on Python 3.14.
This package is not published to a package index. Install it from a git clone:
git clone <this-repository-url> geometry-repair-cli
cd geometry-repair-cli
python -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
pip install -e .Verify the install:
geometry-repair versionThe tool is also runnable as a module, which is handy when you would rather not
put the virtual environment's bin directory on your PATH:
python -m geometry_repair versionTo run the tests, install the dev extra and invoke pytest:
pip install -e ".[dev]"
pytestRepair a dataset and write a GeoPackage:
geometry-repair run dirty.gpkg clean.gpkgLook at what is wrong without writing anything:
geometry-repair inspect dirty.gpkgRepair, then refuse to succeed unless the output is completely valid:
geometry-repair run dirty.gpkg clean.gpkg --verifyReproject, snap coordinates to a millimetre grid, drop slivers, and write a GeoParquet file with an HTML report next to it:
geometry-repair run dirty.shp clean.parquet \
--target-crs EPSG:3857 \
--grid-size 0.001 \
--min-area 0.01 \
--min-polsby-popper 0.02 \
--verify \
--report html --report-path report.htmlFrom Python:
from geometry_repair import RepairPipeline, default_config
config = default_config(chunk_size=50_000, verify=True)
report = RepairPipeline(config).run("dirty.gpkg", "clean.gpkg")
print(report.before.invalid, "->", report.after.invalid)
print(report.stage("dedupe").dropped, "duplicates removed")The repository ships a runnable example that needs no network access and no reference data. It synthesizes a deliberately broken dataset — bow-tie polygons, rings built from unclosed coordinate lists, duplicate rings, degenerate vertex counts, hairline slivers, exact and near-duplicate features, 3D coordinates, multi-part geometry, null geometry, and a companion file in a different CRS — and then repairs it.
python examples/synthesize.py # write the broken files only
python examples/repair_example.py # synthesize, repair, and reportInspecting the broken input reports what is wrong and changes nothing:
$ geometry-repair inspect examples/output/broken_3857.gpkg
Geometry repair summary
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Metric ┃ Before ┃ After ┃ Delta ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ Features │ 13 │ 13 │ +0 │
│ Valid │ 9 │ 9 │ +0 │
│ Invalid │ 4 │ 4 │ +0 │
│ Empty │ 0 │ 0 │ │
│ CRS │ EPSG:3857 │ EPSG:3857 │ │
│ Total area │ 64.005 │ 64.005 │ 0 (+0.0000%) │
│ Total length │ 150.7289 │ 150.7289 │ 0 │
└──────────────┴───────────┴───────────┴──────────────┘
Invalidity by reason
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┓
┃ Reason ┃ Before ┃ After ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━┩
│ missing │ 1 │ 1 │
│ self_intersection │ 2 │ 2 │
│ too_few_points │ 1 │ 1 │
└───────────────────┴────────┴───────┘Repairing it produces the before/after report in full:
$ geometry-repair run examples/output/broken_3857.gpkg clean.gpkg \
--grid-size 0.001 --min-area 0.01 --min-polsby-popper 0.02 --verify
Geometry repair summary
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ Metric ┃ Before ┃ After ┃ Delta ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ Features │ 13 │ 9 │ -4 │
│ Valid │ 9 │ 9 │ +0 │
│ Invalid │ 4 │ 0 │ -4 │
│ Empty │ 0 │ 0 │ │
│ CRS │ EPSG:3857 │ EPSG:3857 │ │
│ Total area │ 64.005 │ 34 │ -30.005 (-46.8792%) │
│ Total length │ 150.7289 │ 64.7279 │ -86.001 │
└──────────────┴───────────┴───────────┴─────────────────────┘
Invalidity by reason
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┓
┃ Reason ┃ Before ┃ After ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━┩
│ missing │ 1 │ 0 │
│ self_intersection │ 2 │ 0 │
│ too_few_points │ 1 │ 0 │
└───────────────────┴────────┴───────┘
Geometry types
┏━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┓
┃ Type ┃ Before ┃ After ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━┩
│ LineString │ 1 │ 1 │
│ MultiPolygon │ 1 │ 0 │
│ None │ 1 │ 0 │
│ Point │ 1 │ 1 │
│ Polygon │ 9 │ 7 │
└──────────────┴────────┴───────┘
Stages
┏━━━━━━━━━━━━━━━━━━━┳━━━━┳━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Stage ┃ In ┃ Out ┃ Changed ┃ Dropped ┃ Added ┃ Seconds ┃ Details ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━╇━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ drop_empty │ 13 │ 12 │ 0 │ 1 │ 0 │ 0.000 │ missing=1 │
│ force_2d │ 12 │ 12 │ 1 │ 0 │ 0 │ 0.000 │ flattened=1 │
│ close_rings │ 12 │ 12 │ 0 │ 0 │ 0 │ 0.000 │ │
│ normalize_crs │ 12 │ 12 │ 0 │ 0 │ 0 │ 0.000 │ source_crs[EPSG:3857=12], │
│ │ │ │ │ │ │ │ target_crs=EPSG:3857 │
│ validate │ 12 │ 12 │ 0 │ 0 │ 0 │ 0.000 │ reasons[self_intersection=2,│
│ │ │ │ │ │ │ │ too_few_points=1], │
│ │ │ │ │ │ │ │ invalid=3, valid=9 │
│ make_valid │ 12 │ 10 │ 3 │ 2 │ 0 │ 0.001 │ attempted=3, │
│ │ │ │ │ │ │ │ method=structure, │
│ │ │ │ │ │ │ │ repaired=3, │
│ │ │ │ │ │ │ │ collapsed_dropped=2 │
│ snap_slivers │ 10 │ 9 │ 2 │ 1 │ 0 │ 0.001 │ snapped=2, grid_size=0.001, │
│ │ │ │ │ │ │ │ sliver_parts_removed=1, │
│ │ │ │ │ │ │ │ emptied_dropped=1 │
│ explode_multipart │ 9 │ 11 │ 2 │ 0 │ 2 │ 0.001 │ multipart_features=2, │
│ │ │ │ │ │ │ │ parts=11 │
│ dedupe │ 11 │ 9 │ 0 │ 2 │ 0 │ 0.000 │ duplicates_removed=2, │
│ │ │ │ │ │ │ │ mode=streaming, │
│ │ │ │ │ │ │ │ unique_keys=9 │
└───────────────────┴────┴─────┴─────────┴─────────┴───────┴─────────┴─────────────────────────────┘
Verification passed: no invalid geometries remain.Reading that report row by row: one null geometry was dropped, one 3D feature was flattened, three invalid geometries were repaired and two of them collapsed to nothing in the process (the duplicate-ring polygon and the degenerate triangle enclose no area, so there was nothing to keep), one hairline sliver was removed, two multi-part features exploded into their four parts, and two duplicates — one exact, one produced by exploding — were removed. The 46.9% area drop is almost entirely the duplicate square that is no longer counted twice.
The example script also writes report.html and report.json into
examples/output.
Repair a dataset and write the cleaned result. INPUT may be a file, a
directory, or a glob pattern; a directory or glob is read as a single stream,
which is how mixed-CRS input sets are normalized in one pass. The output format
is chosen from the OUTPUT file suffix.
| Option | Default | Description |
|---|---|---|
--config, -c |
none | YAML pipeline configuration file. |
--only |
none | Comma-separated stages to run exclusively. |
--skip |
none | Comma-separated stages to disable. |
--target-crs |
none | Reproject output to this CRS. |
--assume-crs |
none | CRS to assume when the input declares none. |
--make-valid-method |
structure |
structure or linework. |
--grid-size |
0 |
Coordinate precision grid, in CRS units. |
--min-area |
0 |
Drop polygonal parts below this area. |
--min-polsby-popper |
0 |
Drop polygonal parts thinner than this (0-1). |
--fuzzy-dedupe |
off | Enable fuzzy deduplication. |
--dedupe-tie-break-column |
none | Attribute deciding which duplicate survives. |
--dedupe-tie-break |
first |
max, min, first, or last. |
--chunk-size |
100000 |
Rows processed per chunk. |
--verify |
off | Re-validate the output; exit 3 if anything is invalid. |
--layer / --output-layer |
none | Layer names for multi-layer formats. |
--report |
table |
table, json, or html. |
--report-path |
none | Write the report to a file. |
--verbose, -v / --quiet, -q |
off | Logging verbosity. |
CLI flags override the corresponding option in a config file, and enable the stage they belong to.
Report on a dataset's validity without writing anything. Exits 0 whenever the
input could be read, however broken it is. Accepts --chunk-size, --layer,
--report, --report-path, and the logging flags.
Exit 3 if the dataset contains any invalid geometry, 0 otherwise. This is the CI
gate. Accepts the same options as inspect.
List the available stages and their options, in default execution order. Pass a
stage name for just that stage, or --yaml to print the default pipeline as a
ready-to-edit config file:
geometry-repair stages --yaml > pipeline.yamlPrint the installed version.
Stages are individually toggleable and independently configurable. Each reports
what it changed, and each is a class implementing the same small protocol, so
adding your own is a matter of subclassing BaseStage and registering it.
The default order is the order below. The reasoning is in How it works.
Removes features with missing or empty geometry.
| Option | Type | Default | Description |
|---|---|---|---|
drop_missing |
bool |
True |
Drop features with null geometry. |
drop_empty |
bool |
True |
Drop features with empty geometry. |
Drops Z and M ordinates, leaving planar geometry. Takes no options.
Closes polygon rings whose first and last vertex differ, and counts how many were found. GEOS closes rings implicitly when parsing most file formats, so this is usually a no-op on file input; it matters for geometry constructed in memory and as an explicit record that the input had the problem.
| Option | Type | Default | Description |
|---|---|---|---|
report_only |
bool |
False |
Count unclosed rings without repairing them. |
Reprojects onto a single target CRS and records a histogram of the source CRS seen across all inputs.
| Option | Type | Default | Description |
|---|---|---|---|
target |
str | None |
None |
Target CRS. Defaults to the first CRS seen. |
on_missing_crs |
str |
fail |
Policy when input has no CRS: fail or assume. |
assume_crs |
str | None |
None |
CRS to assume when input declares none. |
Classifies invalidity by reason without modifying anything. Reasons are
normalized onto stable slugs — self_intersection, ring_self_intersection,
duplicate_rings, too_few_points, invalid_coordinate, hole_outside_shell,
nested_holes, disconnected_interior, nested_shells, ring_not_closed,
other — plus missing for null geometry and nan_coords for non-finite
coordinates.
| Option | Type | Default | Description |
|---|---|---|---|
on_invalid |
str |
ignore |
ignore, warn, or fail. |
max_examples |
int |
2 |
Example geometries recorded per reason, as WKT. |
example_wkt_chars |
int |
120 |
Truncation length for those examples. |
Repairs invalid geometries with shapely.make_valid. Only geometries that
actually fail validation are passed through the repair call.
| Option | Type | Default | Description |
|---|---|---|---|
method |
str |
structure |
structure dissolves and rebuilds; linework preserves every input edge. |
keep_collapsed |
bool |
False |
Keep lower-dimension collapse products (structure only). |
buffer_zero_fallback |
bool |
True |
Retry still-invalid geometries with buffer(0). |
on_collapse |
str |
drop |
Policy for geometries that collapse to empty: drop or keep. |
drop_nan_coords |
bool |
True |
Drop features whose coordinates are not finite. |
structure is the default because it is the right choice for area data: a
bow-tie becomes two clean triangles. Choose linework when you need every input
edge preserved and can accept a GeometryCollection result.
Reduces coordinate precision and removes sliver parts. All thresholds default to zero, which makes the stage a no-op until you tune it — the right values depend entirely on your data's real precision.
| Option | Type | Default | Description |
|---|---|---|---|
grid_size |
float |
0.0 |
Coordinate precision grid, in CRS units. 0 disables. |
precision_mode |
str |
valid_output |
valid_output, pointwise, or keep_collapsed. |
min_area |
float |
0.0 |
Minimum polygonal part area, in squared CRS units. |
min_polsby_popper |
float |
0.0 |
Minimum compactness for a part, 0-1. |
remove_repeated_points_tolerance |
float |
0.0 |
Tolerance for repeated-point removal. |
drop_emptied |
bool |
True |
Drop features that lost all their parts. |
The Polsby-Popper score is 4 * pi * area / perimeter²: 1.0 for a circle, near
zero for a hairline sliver. It is the better sliver test, because a small
compact polygon is usually a real feature while a small thin one usually is
not. Filtering applies to polygonal parts only; points and lines are never
removed by it.
Splits multi-part geometries into one feature per part, copying attributes onto every part.
| Option | Type | Default | Description |
|---|---|---|---|
index_column |
str | None |
None |
Column to record the source feature index in. |
Removes duplicate features. Exact matching hashes the normalized WKB, so two geometries that differ only in vertex order or ring orientation still match. Fuzzy matching snaps centroids to a grid and confirms candidates by intersection-over-union.
| Option | Type | Default | Description |
|---|---|---|---|
exact |
bool |
True |
Deduplicate by normalized geometry digest. |
fuzzy |
bool |
False |
Deduplicate by centroid grid plus IoU. |
fuzzy_grid_size |
float |
1.0 |
Centroid snapping grid size, in CRS units. |
min_iou |
float |
0.98 |
Minimum IoU for a fuzzy match. |
max_candidates_per_cell |
int |
64 |
Retained representatives per grid cell. |
tie_break_column |
str | None |
None |
Attribute deciding which duplicate survives. |
tie_break |
str |
first |
max, min, first, or last. |
The tie-break rule changes how the stage uses memory. With first (the default)
it streams: only a set of seen keys is held, and survivors are emitted with the
chunk they arrived in. With max, min, or last the winner cannot be known
until the input is exhausted, so survivors are buffered and emitted at the end.
A pipeline is a YAML document with a stages: list. Options are validated with
pydantic, so an unknown stage, an unknown option, or an out-of-range value is
reported as a configuration error before any data is read.
chunk_size: 50000
verify: true
stages:
- name: drop_empty
- name: force_2d
- name: close_rings
- name: normalize_crs
options:
target: EPSG:3857
on_missing_crs: assume
assume_crs: EPSG:4326
- name: validate
options:
on_invalid: warn
max_examples: 3
- name: make_valid
options:
method: structure
buffer_zero_fallback: true
on_collapse: drop
- name: snap_slivers
options:
grid_size: 0.001
min_area: 0.01
min_polsby_popper: 0.02
- name: explode_multipart
- name: dedupe
options:
exact: true
fuzzy: true
fuzzy_grid_size: 1.0
min_iou: 0.99
tie_break_column: priority
tie_break: minA stage can be kept in the file but switched off, which keeps the pipeline self-documenting:
- name: close_rings
enabled: falseBare stage names are accepted as shorthand for a stage with default options:
stages: [drop_empty, make_valid, dedupe]Run it with:
geometry-repair run dirty.gpkg clean.gpkg --config pipeline.yamlStage order in the file is the order they execute in. geometry-repair stages --yaml prints the default pipeline as a starting point.
Read and write: GeoPackage (.gpkg), GeoParquet (.parquet, .geoparquet,
.pq), FlatGeobuf (.fgb), GeoJSON (.geojson, .json), newline-delimited
GeoJSON (.geojsonl), and Shapefile (.shp).
Writing to a shapefile emits a warning when attribute names exceed the dBase 10-character limit, including a specific warning when two names truncate to the same thing and GDAL will therefore rename one of them. Prefer GeoPackage or GeoParquet if you can.
Every stage operates on whole numpy object arrays through the Shapely 2.x array
API — shapely.is_valid, shapely.make_valid, shapely.set_precision,
shapely.get_parts, shapely.union_all, and friends — rather than calling into
GEOS once per row. There is no .apply(), .iterrows(), or .itertuples() on a
frame anywhere in the package, and tests/test_design.py fails the build if any
appears.
This is a correctness point as much as a performance one. The vectorized calls are a thin wrapper over the same GEOS entry points that per-row code would use, but they handle null geometry uniformly instead of leaving each call site to remember to. Most of the subtle bugs in hand-rolled cleaning scripts are missing null checks.
Two places deserve a specific mention:
- Reassembling filtered multi-part geometries. After
snap_sliversdrops some parts, the survivors are packed into a padded 2-D object array and unioned alongaxis=1, so reassembly stays a single array call rather than a loop over features. Only features that actually lost a part are rebuilt. - Detecting what precision reduction changed.
shapely.set_precisionrewrites every geometry it touches into a canonical ring order, so comparing input to output withequals_exactreports harmless rotations as changes. The stage instead checks whether any vertex was off the grid to begin with, or whether the vertex count changed.
The default order is drop_empty, force_2d, close_rings, normalize_crs,
validate, make_valid, snap_slivers, explode_multipart, dedupe.
drop_emptyfirst so that null geometry never reaches a later stage, where every one of them would have to special-case it.force_2dbeforededupe, because deduplication hashes WKB and WKB encodes Z. Two features identical on the plane but differing in elevation would otherwise both survive.close_ringsbeforemake_valid, so ring closure is recorded as its own countable event rather than disappearing into a general repair.normalize_crsbefore anything with a tolerance. The precision grid size and the sliver area threshold are both expressed in CRS units. Until the CRS is settled those numbers are meaningless —grid_size: 0.001is a millimetre in a projected CRS and roughly 100 metres in degrees.validatebeforemake_valid, so the report can say what was wrong, not merely that something was.make_validbeforesnap_slivers, because area and compactness measurements on an invalid polygon are not meaningful — an invalid bow-tie measures zero area and would be misread as a sliver.explode_multipartaftermake_valid, because repair frequently produces collections; exploding afterwards is what turns them back into clean single parts.dedupelast, because two features are only reliably comparable once they share a CRS, a coordinate precision, and a multi-part convention. Deduplicating first would miss duplicates that only become identical after cleaning — which is exactly what happens in the worked example above.
You are not stuck with this order: the config file's stage order is the execution order.
Input is read in bounded row chunks (--chunk-size), so peak memory does not
scale with file size. OGR-backed formats are read with skip_features and
max_features so only one chunk is materialized at a time; GeoParquet is read
whole and then sliced.
Output is appended chunk by chunk for drivers that support it (GeoPackage, newline-delimited GeoJSON) and buffered until close for those that do not (GeoParquet, Shapefile, FlatGeobuf, GeoJSON).
Chunking never changes the result. Stages that need to see the whole dataset —
dedupe, and normalize_crs when it is adopting a target CRS — carry state
across chunks, and a stage that buffers gets its held-back features pushed
through the remainder of the pipeline when the input is exhausted. A test repairs
the same dataset at five different chunk sizes and asserts the outputs are
identical.
The pipeline, not the stages, computes the before/after figures: it snapshots
every chunk on the way in and on the way out. That keeps the report accurate even
for a pipeline with no validate stage configured, and means a stage cannot
misreport what happened to the data.
A report carries per-stage input/output counts, validity counts by reason before
and after, geometry-type histograms before and after, source and target CRS,
total area and length deltas, per-stage wall time, and any warnings. It renders
three ways: a rich terminal table, --report json, and --report html. The HTML
is fully self-contained — no external stylesheets, scripts, fonts, or images — so
it can be attached to a CI job or emailed and still render, and it follows the
viewer's light or dark theme.
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Unexpected runtime error. |
| 2 | Command-line usage error. |
| 3 | Invalid geometries remain after repair, or were found by verify. |
| 4 | Configuration or input error. |
--verify re-reads the written output from disk and re-runs validation on it,
rather than trusting the in-memory result, so anything the output format itself
does to the geometry is caught too. It is an idempotency check: repairing an
already-repaired dataset should change nothing and must stay valid.
Gate a pipeline on clean geometry:
geometry-repair run source.gpkg build/clean.gpkg --config pipeline.yaml --verifyOr check a dataset you expect to already be clean, and fail the build if not:
geometry-repair verify data/boundaries.gpkgAs a GitHub Actions step, producing an HTML report as a build artifact:
- name: Repair and verify geometry
run: |
geometry-repair run data/source.gpkg build/clean.gpkg \
--config pipeline.yaml \
--verify \
--report html --report-path build/geometry-report.html
- name: Upload geometry report
if: always()
uses: actions/upload-artifact@v4
with:
name: geometry-report
path: build/geometry-report.htmlTo distinguish a real failure from a usage mistake, branch on the code:
geometry-repair verify data/boundaries.gpkg
case $? in
0) echo "clean" ;;
3) echo "invalid geometry found"; exit 1 ;;
*) echo "the tool could not run"; exit 1 ;;
esac- Area and length are reported in CRS units. In a geographic CRS that means square degrees, which are not a useful measure of area. Reproject to a suitable projected CRS before reading much into the area delta.
- Buffered output formats hold the result in memory. GeoParquet, Shapefile,
FlatGeobuf, and GeoJSON have no append path, so
--chunk-sizebounds read memory but not write memory for those formats. GeoPackage streams end to end. - Buffered deduplication holds the output in memory. A
tie_breakofmax,min, orlastcannot know the winner until the input ends. Use the defaultfirstfor very large inputs. - Fuzzy deduplication is approximate and order-dependent. It compares each
feature against retained representatives in its own and adjacent grid cells,
capped by
max_candidates_per_cell. It is not a full pairwise comparison, and a chain of features each similar to the next may not collapse to one. - Mixed geometry types in one output layer. Writing points, lines, and polygons into a single GeoPackage layer works, but GDAL warns that the result is not strictly conformant. Split by geometry type first if that matters.
make_validcan change area. That is the point — an invalid bow-tie measures zero area and a valid one does not — but it means the area delta is not a reliable error signal on its own. Read it alongside the per-stage counts.- Attribute-level cleaning is out of scope. This tool repairs geometry. Nothing here validates, normalizes, or deduplicates attribute values, beyond using one column as a deduplication tie-break.
Background on the techniques this tool automates: repairing geometry with Shapely and GeoPandas, normalizing CRS across mixed datasets, and spatial deduplication and topology simplification.
MIT. See LICENSE.
Maintained by Geospatial ETL.