Skip to content

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:

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)
rejected: polygon rings require at least three coordinates, got 2
rejected: coordinate must be finite

What constructors do not do is enforce OGC 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:

import gometry as gm

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

Constructors and parsers can admit structurally valid but topologically invalid geometry. Foreign data can arrive through gm.from_wkt, gm.from_wkb, or gm.from_geojson. Either way, you detect invalidity with validate() and fix it with 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() converts it to a Point. remove_repeated_points and orient_polygons can canonicalize valid redundancy when required.

Diagnosing: is_valid and validate

The Geometry.is_valid property returns a plain bool; vectorized code uses the GeometryArray.is_valid mask. validate returns a ValidationReport without raising. The report supports branching on validity, logging diagnostics, and repairing only bad records (GeometryArray.validate() yields one report per element).

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)
invalid: exterior ring has a self-intersection
valid box → True

The returned ValidationReport exposes .valid, .reason (a human-readable message that names the actual fault — e.g. a self-intersection in the exterior ring), .location (the (x, y) coordinate of the first problem), .path (a structural path to it, e.g. '$.shell'), and the stable .code and human-readable .detail 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, is_ring, validate, require, and self_intersections all use the same geographic frame. repair validates the quotient result before returning it, and snap_to_grid(..., repair=True) checks each snapped result in that same frame.

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

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

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

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

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())
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. 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. 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; native geometries and EWKT/EWKB embedded SRIDs raise CRSMismatchError.

InvalidGeometryError remains the exception for invalid geometry or mismatched axes.

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)
8217.7 km²

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

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

import gometry as gm
noisy = gm.LineString([(1.11119, 2.22229), (3.33339, 4.44449)])
print((noisy.quantize(2)).to_wkt())
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.

import gometry as gm
dup = gm.LineString([(0, 0), (0, 0), (1, 1), (1, 1), (2, 2)])
print(dup.remove_repeated_points().to_wkt())
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 and 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). Prefer a precision workflow followed by validate / repair rather than treating quantize as a universal sliver cure — see constructive operations.

A typical ingest pipeline

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

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)
MultiPolygon valid: True

Parse failures (ParseError) stay separate from geometric invalidity (InvalidGeometryError / repair). The detect → repair → assert flow keeps invalid geometry from ever reaching your predicates, overlays, or measurements, where it would otherwise produce unexpected answers. See 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 for when you must call split_antimeridian yourself.

See also