Skip to content

geospatial-etl/geometry-repair-cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

geometry-repair-cli

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.

The problem

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.

Requirements

Python 3.11 or newer. All dependencies install cleanly on Python 3.14.

Install from a clone

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 version

The 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 version

To run the tests, install the dev extra and invoke pytest:

pip install -e ".[dev]"
pytest

Quickstart

Repair a dataset and write a GeoPackage:

geometry-repair run dirty.gpkg clean.gpkg

Look at what is wrong without writing anything:

geometry-repair inspect dirty.gpkg

Repair, then refuse to succeed unless the output is completely valid:

geometry-repair run dirty.gpkg clean.gpkg --verify

Reproject, 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.html

From 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")

Worked example

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 report

Inspecting 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.

Command reference

geometry-repair run INPUT 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.

geometry-repair inspect INPUT

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.

geometry-repair verify INPUT

Exit 3 if the dataset contains any invalid geometry, 0 otherwise. This is the CI gate. Accepts the same options as inspect.

geometry-repair stages [STAGE]

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.yaml

geometry-repair version

Print the installed version.

Stage reference

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.

drop_empty

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.

force_2d

Drops Z and M ordinates, leaving planar geometry. Takes no options.

close_rings

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.

normalize_crs

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.

validate

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.

make_valid

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.

snap_slivers

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.

explode_multipart

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.

dedupe

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.

Config file reference

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: min

A stage can be kept in the file but switched off, which keeps the pipeline self-documenting:

  - name: close_rings
    enabled: false

Bare 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.yaml

Stage order in the file is the order they execute in. geometry-repair stages --yaml prints the default pipeline as a starting point.

Supported formats

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.

How it works

Everything is vectorized

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_slivers drops some parts, the survivors are packed into a padded 2-D object array and unioned along axis=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_precision rewrites every geometry it touches into a canonical ring order, so comparing input to output with equals_exact reports harmless rotations as changes. The stage instead checks whether any vertex was off the grid to begin with, or whether the vertex count changed.

Why the stages run in this order

The default order is drop_empty, force_2d, close_rings, normalize_crs, validate, make_valid, snap_slivers, explode_multipart, dedupe.

  1. drop_empty first so that null geometry never reaches a later stage, where every one of them would have to special-case it.
  2. force_2d before dedupe, because deduplication hashes WKB and WKB encodes Z. Two features identical on the plane but differing in elevation would otherwise both survive.
  3. close_rings before make_valid, so ring closure is recorded as its own countable event rather than disappearing into a general repair.
  4. normalize_crs before 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.001 is a millimetre in a projected CRS and roughly 100 metres in degrees.
  5. validate before make_valid, so the report can say what was wrong, not merely that something was.
  6. make_valid before snap_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.
  7. explode_multipart after make_valid, because repair frequently produces collections; exploding afterwards is what turns them back into clean single parts.
  8. dedupe last, 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.

Chunked processing

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 report

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.

CI usage and exit codes

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 --verify

Or check a dataset you expect to already be clean, and fail the build if not:

geometry-repair verify data/boundaries.gpkg

As 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.html

To 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

Limitations

  • 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-size bounds read memory but not write memory for those formats. GeoPackage streams end to end.
  • Buffered deduplication holds the output in memory. A tie_break of max, min, or last cannot know the winner until the input ends. Use the default first for 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_valid can 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.

Further reading

Background on the techniques this tool automates: repairing geometry with Shapely and GeoPandas, normalizing CRS across mixed datasets, and spatial deduplication and topology simplification.

License

MIT. See LICENSE.


Maintained by Geospatial ETL.

About

Batch geometry cleaning for vector datasets: a vectorized, configurable pipeline that validates, repairs, reprojects, snaps slivers and deduplicates any GeoPandas-readable file, emitting a cleaned dataset plus a before/after validity report and a CI-ready exit code.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages