Source: https://gometry.monicz.dev/guide/validation/

# Validation & repair

Real-world geometry includes self-intersections, disconnected interiors,
non-finite coordinates, missing CRS metadata, and excess coordinate precision.

## Construction validates structure, not topology

Constructors reject *structurally* malformed input immediately. Non-finite
coordinates and rings with too few vertices have no valid interpretation and
raise [`InvalidGeometryError`](https://gometry.monicz.dev/api/errors/#gometry.InvalidGeometryError "InvalidGeometryError"):

```python
import gometry as gm

for ring in ([(0, 0), (1, 0)], [(0, 0), (1, 0), (float("nan"), 1)]):
    try:
        gm.Polygon(ring)
    except gm.InvalidGeometryError as e:
        print("rejected:", e)
```

```text
rejected: polygon rings require at least three coordinates, got 2
rejected: coordinate must be finite
```

What constructors do **not** do is enforce [OGC](https://www.ogc.org/standard/sfa/) *topological* validity. A ring
that closes cleanly but self-intersects — a "bowtie" — is a well-formed
*structure* with an ill-formed *shape*, so it is accepted as an **invalid
geometry** rather than rejected:

```python
import gometry as gm

bowtie = gm.Polygon([(0, 0), (2, 2), (2, 0), (0, 2)])
print("built:", bowtie.geometry_type, "| valid:", bowtie.validate().valid)
```

```text
built: Polygon | valid: False
```

Constructors and parsers can admit structurally valid but topologically invalid
geometry. Foreign data can arrive through [`gm.from_wkt`](https://gometry.monicz.dev/api/toplevel/io-interop/#gometry.from_wkt "from_wkt"),
[`gm.from_wkb`](https://gometry.monicz.dev/api/toplevel/io-interop/#gometry.from_wkb "from_wkb"), or [`gm.from_geojson`](https://gometry.monicz.dev/api/toplevel/io-interop/#gometry.from_geojson "from_geojson").
Either way, you **detect** invalidity with
[`validate()`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.validate "validate") and **fix** it with [`repair()`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.repair "repair").

Cosmetic noise that does *not* change the shape is, by contrast, **valid**: a
repeated consecutive vertex does not itself invalidate a line with at least two
distinct XY positions or a ring with at least three; a clockwise shell is also
valid because ring orientation does not affect validity. A nonempty line whose
coordinates all share one XY position is invalid;
[`repair()`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.repair "repair") converts it to a Point.
[`remove_repeated_points`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.remove_repeated_points "remove_repeated_points") and
[`orient_polygons`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.orient_polygons "orient_polygons") can canonicalize valid
redundancy when required.

## Diagnosing: `is_valid` and `validate`

The [`Geometry.is_valid`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.is_valid "is_valid property") property returns a plain
`bool`; vectorized code uses the [`GeometryArray.is_valid`](https://gometry.monicz.dev/api/geometry/geometryarray/#gometry.GeometryArray.is_valid "is_valid property") mask.
`validate` returns a
[`ValidationReport`](https://gometry.monicz.dev/api/results/#gometry.ValidationReport "ValidationReport") without raising. The report
supports branching on validity, logging diagnostics, and repairing only bad records
([`GeometryArray.validate()`](https://gometry.monicz.dev/api/geometry/geometryarray/#gometry.GeometryArray.validate "validate") yields one report per element).

```python
import gometry as gm

bowtie = gm.Polygon([(0, 0), (2, 2), (2, 0), (0, 2)])

try:
    gm.require(bowtie)
except gm.InvalidGeometryError as e:
    print("invalid:", e)

report = (gm.box(0, 0, 1, 1)).validate()
print("valid box →", report.valid)
```

```text
invalid: exterior ring has a self-intersection
valid box → True
```

The returned `ValidationReport` exposes [`.valid`](https://gometry.monicz.dev/api/results/#gometry.ValidationReport.valid "valid property"),
[`.reason`](https://gometry.monicz.dev/api/results/#gometry.ValidationReport.reason "reason property") (a human-readable message that names the actual fault — e.g. a
self-intersection in the exterior ring), [`.location`](https://gometry.monicz.dev/api/results/#gometry.ValidationReport.location "location property") (the `(x, y)` coordinate of
the first problem), [`.path`](https://gometry.monicz.dev/api/results/#gometry.ValidationReport.path "path property") (a structural path to it, e.g. `'$.shell'`),
and the stable [`.code`](https://gometry.monicz.dev/api/results/#gometry.ValidationReport.code "code property") and human-readable
[`.detail`](https://gometry.monicz.dev/api/results/#gometry.ValidationReport.detail "detail property") fields. For
an OGC-valid geometry `.reason`, `.location`, `.path`, `.code`, and `.detail`
are all `None`; they carry detail when topology validation finds a problem.

Geographic validity recognizes longitude aliases, the antimeridian, and poles,
including pole-enclosing shells and holes. Shared seam edges with polygon
interiors on opposite sides are chart cuts; overlapping interiors on the same
side remain invalid.

Coordinate-domain checks also apply:
an out-of-range latitude reports `latitude_out_of_range`, and a once-winding
unmarked ring whose latitude range does not identify one pole reports
`ambiguous_full_turn_ring`. `is_valid` returns `False` for either issue without
raising, and `GeometryArray.is_valid` writes `False` for the affected row.
`validate` reports the issue without raising.

Geographic operations still raise
`InvalidGeometryError` for unsupported coordinates. `repair` raises the same
exception when it cannot reconstruct an out-of-domain coordinate.

For quotient-topology issues, the witness is a canonical coordinate of the
issue's contact set, chosen deterministically by minimum latitude then longitude.

The same coordinates without a CRS, or under a projected CRS, remain ordinary
planar coordinates. Attaching a geographic frame supplies its periodic longitude
seam and pole, while a planar frame has no identified seam.

`is_valid`, [`is_simple`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.is_simple "is_simple property"), [`is_ring`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.is_ring "is_ring property"), `validate`, [`require`](https://gometry.monicz.dev/api/toplevel/constructive-validation/#gometry.require "require"), and
[`self_intersections`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.self_intersections "self_intersections") all use the same
geographic frame. `repair` validates the quotient result before returning it,
and [`snap_to_grid(..., repair=True)`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.snap_to_grid "snap_to_grid") checks
each snapped result in that same frame.

The latitude domain issue retains its source ring path and coordinate witness:

```python
import gometry as gm

polygon = gm.Polygon([(0, 0), (10, 0), (10, 100), (0, 0)], crs=4326)
report = polygon.validate()
print(report.code, report.path, report.location)
print("valid:", polygon.is_valid)
```

```text
latitude_out_of_range $.shell (10.0, 100.0)
valid: False
```

Validation reports and boundary errors

`validate()` returns a report without raising. `require()` raises the
matching domain exception when its validity, CRS, or axes contract fails.
`is_simple` and `is_ring` answer narrower topological questions; neither is
a general structural check or a replacement for polygon validity.

## Fixing: `repair`

When repair succeeds, it returns a valid geometry; it can refuse input it
cannot reconstruct. It offers two strategies via `method`:

| `method` | Strategy |
| --- | --- |
| `'linework'` (default) | re-node the boundary **linework** and reassemble — boundary-faithful |
| `'structure'` | rebuild valid polygonal **areas** from the input — area-faithful |

The bowtie repairs into a valid geometry:

```python
import gometry as gm

bowtie = gm.Polygon([(0, 0), (2, 2), (2, 0), (0, 2)])

fixed = bowtie.repair()
print(fixed.geometry_type, "valid:", fixed.validate().valid)

fixed_struct = bowtie.repair(method="structure")
print(fixed_struct.geometry_type, "valid:", fixed_struct.validate().valid)
```

```text
MultiPolygon valid: True
MultiPolygon valid: True
```

invalid parcel

The parcel boundary crosses itself at the center, making the bowtie polygon invalid.

repaired parcel pieces

Repair splits the self-crossing bowtie into valid polygonal pieces.

- input
- result

The two strategies genuinely differ once regions overlap: `linework` folds
by even-odd parity (a doubly-covered overlap cancels), `structure` unions the
enclosed areas:

```python
import gometry as gm

overlap = gm.from_wkt(
    "MULTIPOLYGON (((0 0, 4 0, 4 4, 0 4, 0 0)), ((2 2, 6 2, 6 6, 2 6, 2 2)))"
)
print("linework:", overlap.repair().area)
print("structure:", overlap.repair(method="structure").area)
```

```text
linework: 28.0
structure: 28.0
```

A collapsed line repairs to its first coordinate as a Point. Collapsed
MultiLineString members become Point or MultiPoint values; surviving linework
is retained alongside them in a GeometryCollection:

```python
import gometry as gm

collapsed = gm.from_wkt("LINESTRING (2 3, 2 3)")
print(collapsed.is_valid, collapsed.validate().code, collapsed.repair().to_wkt())

mixed = gm.from_wkt("MULTILINESTRING ((0 0, 0 0), (1 1, 2 2))")
print(mixed.repair().to_wkt())
```

```text
False line_collapsed POINT (2 3)
GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (1 1, 2 2))
```

Already-valid input is returned as-is after the validation check on every surface:
`geom.repair()` and `array.repair()` reuse valid rows. Z/M ordinates are carried
through the rebuild.

For a geographic periodic-seam crossing, the validity check runs before any
rebuild. A valid input therefore keeps its original coordinates exactly; an
invalid crossing is repaired from the seam-split topology. The same
rule powers `snap_to_grid(..., repair=True)`.

`repair` can split, merge, or drop parts to reach validity. Choose the method
whose boundary or area interpretation matches your data.

## Enforcing contracts at the boundary

Beyond OGC validity, a boundary can require a CRS, valid topology, or a
particular dimensionality.
`gm.require` is the single boundary API: it accepts an
existing geometry or an external geometry-like object, returns the geometry
with its coordinate values unchanged on success, and raises the matching
specific exception otherwise.

With `crs=`, it stamps CRS-free coordinates,
relabels an operationally equivalent existing CRS to the requested spelling,
and rejects a conflicting frame with [`CRSMismatchError`](https://gometry.monicz.dev/api/errors/#gometry.CRSMismatchError "CRSMismatchError").
When `crs` is omitted or `None`, an existing CRS is preserved. Coordinate
epochs are preserved and revalidated against the resulting CRS.

The `crs=` parameter uses the `CrsInput` contract documented by
[`CRS`](https://gometry.monicz.dev/api/crs/#gometry.CRS "CRS"). Accepted forms include an EPSG integer, an authority,
WKT, or PROJ string, an `(authority, code)` pair, a PROJJSON or CF mapping,
and an object exposing `to_wkt()`, such as `gometry.CRS` or `pyproj.CRS`.
Conflicting legacy CRS metadata in GeoJSON raises [`ParseError`](https://gometry.monicz.dev/api/errors/#gometry.ParseError "ParseError");
native geometries and EWKT/EWKB embedded SRIDs raise
[`CRSMismatchError`](https://gometry.monicz.dev/api/errors/#gometry.CRSMismatchError "CRSMismatchError").

`InvalidGeometryError` remains the exception for invalid geometry or mismatched
axes.

```python
import gometry as gm

def ellipsoidal_area_km2(poly):
    poly = gm.require(poly, crs=4326)
    return poly.area / 1e6

ok = gm.box(2, 48, 3, 49, crs=4326)
print(f"{ellipsoidal_area_km2(ok):.1f} km²")

try:
    ellipsoidal_area_km2(gm.box(2, 48, 3, 49))
except gm.CRSMismatchError as e:
    print("rejected:", e)
```

```text
8217.7 km²
```

The optional `crs=` and `axes=` keywords add frame and storage contracts to
that same validation call:

```python
import gometry as gm

raw = "POLYGON ((2 48, 3 48, 3 49, 2 49, 2 48))"
area = gm.require(raw, crs=4326, axes="XY")
print("meets contract:", area.crs == gm.CRS(4326), area.coordinate_axes)
```

```text
meets contract: True XY
```

Use `axes='xy'` when your storage accepts only 2D geometry.

## Cleaning: `quantize` and `remove_repeated_points`

Floating-point coordinates can carry more digits than the data requires, affecting
snapping and overlay results. Two targeted cleaners handle common coordinate noise
without a full repair.

[`geom.quantize(precision)`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.quantize "quantize") rounds every coordinate to `precision` decimal
places. It gives coordinates from different sources a common precision for
overlay, equality, deduplication, and interchange.
Negative precision rounds to tens (`-1`), hundreds (`-2`), and larger decimal
steps. All present X/Y/Z/M ordinates use ties-to-even rounding, as with Python's
`round`; tiny coordinates can use more than 15 decimal places. WKT and WKB
writers' `precision=` applies this same operation before serialization.

```python
import gometry as gm
noisy = gm.LineString([(1.11119, 2.22229), (3.33339, 4.44449)])
print((noisy.quantize(2)).to_wkt())
```

```text
LINESTRING (1.11 2.22, 3.33 4.44)
```

survey track coordinates

The CRS-free survey track contains vertices with three decimal places.

coordinates rounded to 2 decimals

Quantization moves each track vertex to two-decimal coordinates without removing vertices.

- input
- result

`remove_repeated_points` drops consecutive
duplicate vertices. The optional `tolerance` also collapses vertices closer than
that distance, allowing oversampled tracks to be thinned.

```python
import gometry as gm
dup = gm.LineString([(0, 0), (0, 0), (1, 1), (1, 1), (2, 2)])
print(dup.remove_repeated_points().to_wkt())
```

```text
LINESTRING (0 0, 1 1, 2 2)
```

track with repeated vertices

The five-coordinate track repeats its first and middle positions consecutively.

consecutive repeats removed

Removing consecutive repeats leaves the same line path represented by three distinct vertices.

- input
- result

The related tools [`snap`](https://gometry.monicz.dev/api/toplevel/constructive-validation/#gometry.snap "snap") and [`normalize`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.normalize "normalize") handle the other common sources of
geometric noise (near-misses that should coincide, and non-canonical vertex
order / ring orientation).

Precision workflow before overlay

Overlay operations are sensitive to near-coincident vertices. An explicit
`quantize` / `snap_to_grid` pass can help sources align — but it can also
*create* invalidity on tight geometries (see [choosing](https://gometry.monicz.dev/guide/choosing/)). Prefer
a precision workflow followed by `validate` / `repair` rather than
treating quantize as a universal sliver cure — see
[constructive operations](https://gometry.monicz.dev/guide/constructive/#dissolving-many-geometries-union_all).

## A typical ingest pipeline

An ingest pipeline can combine a dimension contract, validity checking, and
repair:

```python
import gometry as gm

def ingest(payload):
    try:
        geom = gm.from_wkt(payload, crs=4326)
    except gm.ParseError:
        raise
    geom = geom.remove_repeated_points()
    if not geom.is_valid:
        geom = geom.repair()
    return gm.require(geom, crs=4326, axes="XY")

result = ingest("POLYGON ((0 0, 2 2, 2 0, 0 2, 0 0))")
print(result.geometry_type, "valid:", result.validate().valid)
```

```text
MultiPolygon valid: True
```

Parse failures ([`ParseError`](https://gometry.monicz.dev/api/errors/#gometry.ParseError "ParseError")) stay separate from geometric invalidity
(`InvalidGeometryError` / repair). The detect → repair → assert flow keeps invalid
geometry from ever reaching your [predicates](https://gometry.monicz.dev/guide/predicates/),
[overlays](https://gometry.monicz.dev/guide/constructive/), or [measurements](https://gometry.monicz.dev/guide/crs/), where it would otherwise
produce unexpected answers. See [Security](https://gometry.monicz.dev/about/security/) for parsing
and resource limits.

Crossing a geographic longitude seam (±180° for EPSG:4326) does not itself make
geometry invalid. gometry's topology operations handle the crossing;
see [Working across the antimeridian](https://gometry.monicz.dev/guide/crs/#across-the-antimeridian) for when
you must call [`split_antimeridian`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.split_antimeridian "split_antimeridian") yourself.

## See also

- [Predicates](https://gometry.monicz.dev/guide/predicates/) — robust boundary semantics on validated input.
- [Geometry](https://gometry.monicz.dev/guide/geometry/) — the Z/M ordinate model and storage contracts.
- [CRS, units & measurement](https://gometry.monicz.dev/guide/crs/#across-the-antimeridian) — reprojection and the antimeridian.
- [Security](https://gometry.monicz.dev/about/security/) — untrusted formats and resource limits.
- [API: Geometry.validate](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.validate "validate") ·
  [Geometry.repair](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.repair "repair") · [require](https://gometry.monicz.dev/api/toplevel/constructive-validation/#gometry.require "require")
