Skip to content

Spatial predicates

Predicates answer yes/no questions about how two geometries relate: does this polygon contain that point? do these roads cross? gometry's predicates evaluate topology in X/Y without transforming coordinates or measuring distance. Both geometries must carry matching CRS and coordinate-epoch metadata; otherwise the operation raises CRSMismatchError.

The predicate vocabulary

Predicate True when… Symmetric?
contains no point of right lies outside left, and their interiors share a point no
contains_properly contains with no boundary contact at all no
within left is contained by right (the inverse of contains) no
covers every point of right is in left (boundary-inclusive) no
covered_by the inverse of covers no
intersects they share any point yes
disjoint they share no point (not intersects) yes
touches they meet only on boundaries, interiors stay apart yes
crosses a dimension-dependent interior crossing; for example, a line has interior parts inside and outside a polygon, or two curves meet at an interior point yes
overlaps same-dimension overlap, neither contains the other yes
equals topologically identical point sets yes
import gometry as gm

region = gm.box(0, 0, 10, 10)
inside = gm.Point(5, 5)
outside = gm.Point(20, 20)

print("contains inside:", gm.contains(region, inside))
print("contains outside:", gm.contains(region, outside))
print("disjoint outside:", gm.disjoint(region, outside))
contains inside: True
contains outside: False
disjoint outside: True
contains(A, B)
A surrounds B, and their interiors overlap.
overlaps(A, B)
A and B share an interior region, but neither contains the other.
touches(A, B)
A and B share one boundary edge while their interiors remain separate.
disjoint(A, B)
A and B share no interior or boundary points.
  • A
  • B

Binary predicates are free functions: gm.contains(left, right). The same spelling handles scalar and array combinations. Repeated probing needs no preparation step: a geometry builds its point index after its first few probes and keeps it.

Vectorization

The module-level predicates are vectorized: when one argument is scalar and the other is a GeometryArray or another gometry geometry sequence, they return a read-only bool_ numpy.ndarray mask evaluated in Rust without the Python per-pair overhead.

import gometry as gm

region = gm.box(0, 0, 10, 10)
probes = gm.GeometryArray([gm.Point(5, 5), gm.Point(20, 20), gm.Point(4, 4)])

hits = gm.contains(region, probes)
print(repr(hits))
print("matching indices:", [i for i, h in enumerate(hits) if h])
array([ True, False,  True])
matching indices: [0, 2]

A sequence argument returns one typed mask from a native batch. Lists, object arrays, and other iterables of Geometry | None are normalized to geometry rows; missing rows remain at their original positions. Broadcasting is strict: scalar-vs-sequence broadcasts, and equal-length sequences are compared pairwise; mismatched non-scalar lengths raise. Many-to-many work uses a gm.SpatialIndex or gm.join.

Coordinate predicates

Use the same predicate with keyword-only x and y when testing raw coordinates. This keeps the point-probe kernel without constructing Point objects:

import gometry as gm

region = gm.box(0, 0, 10, 10)
print(gm.contains(region, x=5, y=5))
print(repr(gm.intersects(region, x=[5, 30], y=[5, 5])))
True
array([ True, False])

Provide either a right-hand geometry or both coordinate keywords. The coordinates are interpreted in the left geometry's CRS. Geographic inputs use the same antimeridian and pole topology as predicates against explicit points, including strict interior versus boundary contact at the frame's periodic seam and poles (±180°/±90° for EPSG:4326).

Robust predicates

Orientation and intersection tests use adaptive-precision arithmetic to resolve collinear, coincident, and nearly degenerate input. Scalar, array, prepared, and index queries use the same exact topology.

Geographic coordinates

In a geographic CRS, topology resolves the CRS-native longitude period, seams, poles, and prime meridian before planar tests. Edges are straight in the source coordinate chart, not geodesics; projection and densification can therefore change the represented shape.

An unmarked once-winding ring is rejected when its latitude range spans both hemispheres or lies entirely on the equator, because neither identifies the enclosed pole. Equator-touching rings in one hemisphere remain admissible. See polar rings and full-turn edges.

Boundary points are not contained

The OGC Simple Features contains relation requires that no point of the candidate lies outside the container and that their interiors share a point. A point exactly on a polygon boundary does not share the polygon's interior, so it fails contains and can satisfy covers. A contained polygon can share part of the container boundary as long as their interiors also overlap.

import gometry as gm

region = gm.box(0, 0, 10, 10)
on_edge = gm.Point(0, 5)

print("contains:", gm.contains(region, on_edge))
print("covers:", gm.covers(region, on_edge))
print("touches:", gm.touches(region, on_edge))
contains: False
covers: True
touches: True

The strictest member of the family is contains_properly: the candidate must lie entirely in the interior, never touching the container's boundary (DE-9IM T**FF*FF*). It excludes shared borders.

import gometry as gm

region = gm.box(0, 0, 10, 10)
inner = gm.box(2, 2, 8, 8)
flush = gm.box(0, 0, 5, 5)

print("contains:", gm.contains(region, flush))
print("contains_properly:", gm.contains_properly(region, flush))
print("properly inside:", gm.contains_properly(region, inner))
contains: True
contains_properly: False
properly inside: True

contains vs covers

covers includes the whole boundary case: a boundary point can be covered without being contained. Use covers when points on tile edges, snapped grids, or shared borders belong to the result.

Empty geometry semantics

Operations can yield an empty geometry (e.g. the intersection of disjoint shapes). Empties have total DE-9IM semantics — they are not "undefined":

Pairing Typical result
equals(empty, empty) True (compatible empty kinds)
disjoint(empty, anything) incl. empty–empty True
contains / within / intersects / touches / … False
import gometry as gm

empty = gm.intersection(gm.box(0, 0, 1, 1), gm.box(5, 5, 6, 6))
other = gm.from_wkt("POINT EMPTY")
print("is_empty:", empty.is_empty)
print("equals two empties:", gm.equals(empty, other))
print("disjoint two empties:", gm.disjoint(empty, other))
print("contains empty:", gm.contains(gm.box(0, 0, 1, 1), empty))
is_empty: True
equals two empties: True
disjoint two empties: True
contains empty: False

Applications that treat emptiness specially can branch on geom.is_empty (for example, to skip rows or substitute a sentinel). The empty-input predicate table defines the behavior.

Predicates ignore Z/M; topology is XY

For truth values, planar predicates evaluate X/Y topology and ignore Z/M. If you need ordinate-level identity, ask for it explicitly:

import gometry as gm
a = gm.Point(1.0, 2.0, z=10.0)
b = gm.Point(1.0, 2.0, z=99.0)
print('equals (XY topology):', gm.equals(a, b))
print('equals_exact (with Z):', gm.equals_exact(a, b, include_z=True))
print('== (value identity):', a == b)
equals (XY topology): True
equals_exact (with Z): False
== (value identity): False

See geometry for the ordinate model.

Equality semantics

equals is a topological test: two geometries are equal if they cover the same point set, regardless of vertex order or starting point. equals_exact is a coordinate test: same structure with every paired ordinate within tolerance (default 0.0, i.e. exact), in the same order — pass tolerance= for floating-point comparison and include_z/include_m to select ordinates. equals_identical (and scalar ==) is full value identity — kind, vertex order, active Z/M, and CRS/epoch — so geometries are hashable; a frame difference returns False rather than raising.

Question API Vertex order Z/M CRS/epoch mismatch
Same spatial point set? gm.equals(a, b) Ignored Ignored by 2D topology Raises
Same coordinate sequence within a tolerance? gm.equals_exact(a, b, tolerance=...) Compared Selected by include_z/include_m Raises
Same complete Python value? gm.equals_identical(a, b) / scalar a == b Compared bit-for-bit Compared Returns False
import gometry as gm
forward = gm.LineString([(0, 0), (1, 1), (2, 2)])
backward = gm.LineString([(2, 2), (1, 1), (0, 0)])
print('equals:', gm.equals(forward, backward))
print('equals_exact:', gm.equals_exact(forward, backward))
print('equals_identical:', gm.equals_identical(forward, backward))
print('==:', forward == backward)
equals: True
equals_exact: False
equals_identical: False
==: False

DE-9IM: the model under the hood

Each named predicate is a shorthand for a pattern over the Dimensionally Extended 9-Intersection Model (DE-9IM). It describes how the interior, boundary, and exterior of two geometries intersect, as a 3×3 matrix flattened to a 9-character string. A result cell is F (no intersection) or 0/1/2 (the intersection's dimension). Pattern matching also accepts T (any intersection) and * (don't care).

relate returns the matrix; relate_pattern tests it against a mask.

import gometry as gm

region = gm.box(0, 0, 10, 10)
probe = gm.Point(5, 5)

matrix = gm.relate(region, probe)
print("DE-9IM:", matrix)

print("matches contains pattern:", gm.relate_pattern(region, probe, "T*****FF*"))
DE-9IM: 0F2FF1FF2
matches contains pattern: True

relate/relate_pattern represent relationships not covered by a named predicate, such as boundaries that touch while interiors are disjoint and both geometries are one-dimensional. One matrix can be tested against several patterns without recomputing the relationship for each named predicate.

MultiLineString boundary semantics

MultiLineString uses the mod-2 boundary rule from OGC Simple Feature Access: an endpoint belongs to the collection boundary only when it occurs in an odd number of member LineString boundaries.

For the chain represented in the table below, (0, 0) and (4, 4) have odd incidence and are boundary points. The shared node (4, 0) has even incidence, so it is in the MultiLineString interior.

Named case relate(point, lines) relate(lines, point)
Odd-incidence boundary (0, 0) F0FFFF102 FF10F0FF2
Even-incidence shared node (4, 0) 0FFFFF102 0F1FF0FF2
Off-line point (7, 9) FF0FFF102 FF1FF00F2

Consequently, contains(lines, point) and contains_properly(lines, point) are true only for the shared interior node; covers(lines, point) is true for both on-line points. The inverse within and covered_by results follow the same distinction. touches is true only for the odd-incidence boundary point, while intersects is true for both on-line points and disjoint is true only for the off-line point.

Planar computation, strict metadata

Predicates read ordinates as a flat plane and require matching CRS and coordinate-epoch metadata. Reprojection can change vertices, seams, and the realized topology. On a geographic CRS, topology resolves longitude aliases and seam and pole equivalence in the CRS-native frame; seam-crossing input is split into chart pieces before construction. Metric behavior is documented in CRS, units & measurement.

See also