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

# Geometry & dimensions

Geometry values are instances of [`Geometry`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry "Geometry"): one of **seven** immutable,
hashable [OGC Simple Features](https://www.ogc.org/standard/sfa/) types, each
optionally carrying a CRS and per-vertex Z (elevation) and M (measure)
ordinates.

| Type | Made of | Constructor |
| --- | --- | --- |
| [`Point`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point "Point") | one coordinate | [`gm.Point`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point "Point") |
| [`LineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.LineString "LineString") | ordered vertices | [`gm.LineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.LineString "LineString") |
| [`Polygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Polygon "Polygon") | a shell + holes | [`gm.Polygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Polygon "Polygon") / [`gm.box`](https://gometry.monicz.dev/api/toplevel/constructors-bulk-factories/#gometry.box "box") |
| [`MultiPoint`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiPoint "MultiPoint") | many points | [`gm.MultiPoint`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiPoint "MultiPoint") |
| [`MultiLineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiLineString "MultiLineString") | many lines | [`gm.MultiLineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiLineString "MultiLineString") |
| [`MultiPolygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiPolygon "MultiPolygon") | many polygons | [`gm.MultiPolygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiPolygon "MultiPolygon") |
| [`GeometryCollection`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.GeometryCollection "GeometryCollection") | any mix | [`gm.GeometryCollection`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.GeometryCollection "GeometryCollection") |

Always (x, y) — lon/lat only under a geographic CRS

Every constructor takes coordinates in **(x, y)** order. Under a
**geographic** CRS those axes are **(longitude, latitude) in that CRS's
native angular units** (such as degrees, radians, or grads). Projected CRSs
use their linear axes; CRS-free geometries use raw coordinates.
Pass `crs=4326` for WGS 84 longitude/latitude in degrees.

## Points

Construct a point with [`gm.Point(x, y)`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point "Point"); under a geographic CRS, `(x, y)` is
`(longitude, latitude)` in that CRS's native angular units.

```python
import gometry as gm

paris = gm.Point(2.3522, 48.8566, crs=4326)
print(paris.to_wkt())
print("x:", paris.x, "y:", paris.y)
print("crs:", paris.crs, "coordinate_axes:", paris.coordinate_axes)
```

```text
POINT (2.3522 48.8566)
x: 2.3522 y: 48.8566
crs: EPSG:4326 coordinate_axes: XY
```

`.x` / `.y` are for single points only

A `Point` exposes [`.x`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point.x "x property"), [`.y`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point.y "y property") (and [`.z`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point.z "z property") / [`.m`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point.m "m property") when present) for scalar
access. For any other geometry, pull every vertex out with
[`geom.coords`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.coords "coords property") (see
[Inspecting geometries](https://gometry.monicz.dev/guide/geometry/#inspecting-geometries)).

## LineStrings

A [`LineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.LineString "LineString") is an ordered sequence of vertices. Pass any sequence of
`(x, y)` pairs or an `(N, 2)` NumPy-style array — gometry copies the coordinates
once into its Rust geometry representation, so large inputs avoid per-point
Python object construction.

```python
import numpy as np
import gometry as gm

coords = np.array([(0, 0), (1, 2), (3, 1), (4, 3)], dtype=float)
line = gm.LineString(coords)
print(line.to_wkt())
print("length:", line.length)
```

```text
LINESTRING (0 0, 1 2, 3 1, 4 3)
length: 6.708203932499369
```

Four ordered LineString vertices

The LineString follows all four vertices in order from left to right.

## Polygons

A [`Polygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Polygon "Polygon") is a **shell** (exterior ring) plus zero or more **holes**
(interior rings). Rings are closed automatically — you do not need to repeat
the first vertex at the end, though it is harmless if you do.

```python
import gometry as gm

square_with_hole = gm.Polygon(
    [(0, 0), (4, 0), (4, 4), (0, 4)],
    holes=[[(1, 1), (2, 1), (2, 2), (1, 2)]],
)
print(square_with_hole.to_wkt())
print("holes:", len(square_with_hole.interiors))
print("area:", square_with_hole.area)
```

```text
POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1))
holes: 1
area: 15.0
```

Shell and interior ring

The polygon shell encloses one square hole.

Constructors check structure, not topology

`gm.Polygon` rejects *structurally* malformed input eagerly — non-finite
coordinates, or a ring with fewer than three vertices, raise a
[`InvalidGeometryError`](https://gometry.monicz.dev/api/errors/#gometry.InvalidGeometryError "InvalidGeometryError").
It does **not** run full OGC validity, so a self-intersecting "bowtie" is
built as an *invalid* geometry rather than rejected. Detect it with
[`geom.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") — see
[Validation](https://gometry.monicz.dev/guide/validation/).

### Boxes

For the common axis-aligned rectangle, [`gm.box`](https://gometry.monicz.dev/api/toplevel/constructors-bulk-factories/#gometry.box "box") is a shortcut
taking `(minx, miny, maxx, maxy)`:

```python
import gometry as gm

extent = gm.box(0, 0, 10, 5)
print(extent.to_wkt())
print("bounds:", extent.bounds)
```

```text
POLYGON ((0 0, 10 0, 10 5, 0 5, 0 0))
bounds: (0.0, 0.0, 10.0, 5.0)
```

## Multi-part geometries

Multi-geometries group several parts of the **same** type.
[`gm.MultiPoint`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiPoint "MultiPoint") accepts a sequence of coordinate pairs:

```python
import numpy as np
import gometry as gm

cloud = gm.MultiPoint(np.random.default_rng(0).random((5, 2)).tolist())
print(cloud.geometry_type, "with", len(cloud), "points")
```

```text
MultiPoint with 5 points
```

[`MultiLineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiLineString "MultiLineString") and [`MultiPolygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiPolygon "MultiPolygon") take sequences of part-coordinate
sequences and part geometries respectively:

```python
import gometry as gm

mls = gm.MultiLineString([
    [(0, 0), (1, 1)],
    [(2, 0), (3, 1)],
])
mp = gm.MultiPolygon([
    gm.box(0, 0, 1, 1),
    gm.box(2, 2, 3, 3),
])
print(mls.to_wkt())
print(mp.to_wkt())
```

```text
MULTILINESTRING ((0 0, 1 1), (2 0, 3 1))
MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)))
```

Explicit empty members are retained in their original order and count. For
example, `gm.MultiLineString([gm.LineString(), [(0, 0), (1, 1)]])` renders as
`MULTILINESTRING (EMPTY, (0 0, 1 1))`; use `.parts` to inspect those typed
empty members. Multipart members must share one coordinate-axis layout.

### GeometryCollections

When parts have **mixed** types, use a [`GeometryCollection`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.GeometryCollection "GeometryCollection"):

```python
import gometry as gm

gc = gm.GeometryCollection([
    gm.Point(0, 0),
    gm.LineString([(1, 1), (2, 2)]),
    gm.box(3, 3, 4, 4),
])
print(gc.to_wkt())
```

```text
GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (1 1, 2 2), POLYGON ((3 3, 4 3, 4 4, 3 4, 3 3)))
```

Collection area counts overlaps once; length sums members

[`.area`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.area "area property") measures the union of a collection's
areal support, so overlapping members count once. [`.length`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.length "length property")
sums every member's length or perimeter, including overlapping members.
Points and lines contribute zero area.
Prefer homogeneous multi-types when you want a single-kind column; reserve
collections for genuinely heterogeneous
results (e.g. an overlay that returns both lines and points, or the
[tessellation operations](https://gometry.monicz.dev/guide/constructive/#tessellation-triangulate-voronoi-polygonize)).

## Inspecting geometries

These properties and functions apply to every geometry type:

| Call | Returns |
| --- | --- |
| [`geom.geometry_type`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.geometry_type "geometry_type property") | the type name, e.g. `"Polygon"` |
| [`geom.coordinate_axes`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.coordinate_axes "coordinate_axes property") | ordinate layout: `"XY"`, `"XYZ"`, `"XYM"`, `"XYZM"` |
| [`geom.is_empty`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.is_empty "is_empty property") | whether the geometry holds no coordinates |
| [`bounds`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.bounds "bounds property") | `(minx, miny, maxx, maxy)` tuple |
| [`geom.coords`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.coords "coords property") | every vertex as a flat, indexable [`Coordinates`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Coordinates "Coordinates") view |
| [`geom.parts`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.parts "parts property") | component geometries of a multi-part |
| [`poly.exterior`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Polygon.exterior "exterior property") / [`poly.interiors`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Polygon.interiors "interiors property") | the shell and hole rings of a [`Polygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Polygon "Polygon") |

```python
import gometry as gm

poly = gm.Polygon(
    [(0, 0), (4, 0), (4, 4), (0, 4)],
    holes=[[(1, 1), (2, 1), (2, 2), (1, 2)]],
)

print("type:", poly.geometry_type)
print("coordinate_axes:", poly.coordinate_axes)
print("bounds:", poly.bounds)
print("vertices:", len(poly.coords))
print("parts:", len(poly.parts))
print("holes:", len(poly.interiors))
```

```text
type: Polygon
coordinate_axes: XY
bounds: (0.0, 0.0, 4.0, 4.0)
vertices: 10
parts: 1
holes: 1
```

`geom.coords` is a flat, indexable `Coordinates` view over **every** vertex
(each ring, each part, flattened depth-first). Index it for tuples, iterate it,
or pull whole axes as columns and hand
them straight to NumPy, pandas, or polars without a Python loop:

```python
import numpy as np
import gometry as gm

coords = gm.LineString([(0, 0), (1, 2), (3, 1)], z=[10.0, 20.0, 30.0]).coords

print("len:", len(coords))
print("first tuple:", coords[0])
print("axes:", coords.coordinate_axes)
print("x column:", repr(np.asarray(coords.x)))  # zero-copy ndarray
print("dict keys:", list(coords.to_dict().keys()))
```

```text
len: 3
first tuple: (0.0, 0.0, 10.0)
axes: XYZ
x column: array([0., 1., 3.])
dict keys: ['x', 'y', 'z']
```

[`.x`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Coordinates.x "x property")/[`.y`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Coordinates.y "y property") (and [`.z`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Coordinates.z "z property")/[`.m`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Coordinates.m "m property") when present) are zero-copy columns; [`.to_dict()`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Coordinates.to_dict "to_dict") returns
read-only NumPy columns such as `{'x': ndarray, 'y': ndarray}` for direct use
with `pd.DataFrame` or `pl.DataFrame` (forced Z/M use `NaN` where absent);
`np.asarray(coords)` materializes an `(N, dims)` array; and [`.row_index`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Coordinates.row_index "row_index property") tags each vertex
with its source row when the geometry is a [`GeometryArray`](https://gometry.monicz.dev/guide/arrays/).

## Geometry types

Every geometry is an instance of a concrete subclass of
[`Geometry`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry "Geometry") — [`Point`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Point "Point"),
[`LineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.LineString "LineString"), [`Polygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.Polygon "Polygon"),
[`MultiPoint`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiPoint "MultiPoint"), [`MultiLineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiLineString "MultiLineString"),
[`MultiPolygon`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.MultiPolygon "MultiPolygon"), or
[`GeometryCollection`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.GeometryCollection "GeometryCollection") — so `isinstance` works and
constructors/operations return the precise type:

```python
import gometry as gm

print(type(gm.Point(1, 2)).__name__)
print(type(gm.box(0, 0, 1, 1)).__name__)
print(isinstance(gm.box(0, 0, 1, 1), gm.Polygon))
```

```text
Point
Polygon
True
```

The multi-part classes are also registered as `collections.abc.Sequence`, so
`isinstance` checks and ABC-based dispatch recognize the sequence interface.
Their static types are `Sequence[Point]`, `Sequence[LineString]`,
`Sequence[Polygon]`, or `Sequence[Geometry]`. Use class patterns such as
`MultiPoint(parts)` for matching; anonymous sequence patterns do not match
geometry objects. NumPy still treats each multipart geometry as one object:
`np.asarray(multi)` has shape `()`, and `np.array([multi, multi], dtype=object)`
has shape `(2,)`.

```python
import gometry as gm

poly = gm.Polygon([(0, 0), (4, 0), (4, 4), (0, 4)], holes=[[(1, 1), (2, 1), (2, 2), (1, 2)]])
print("exterior:", poly.exterior.geometry_type, "| holes:", len(poly.interiors))

mp = gm.MultiPolygon([gm.box(0, 0, 1, 1), gm.box(2, 2, 3, 3)])
print("parts:", len(mp), "| first:", mp[0].geometry_type, "| areas:", [p.area for p in mp])
```

```text
exterior: LineString | holes: 1
parts: 2 | first: Polygon | areas: [1.0, 1.0]
```

Accessing a member on the wrong type raises `AttributeError`: `line.x` fails
because scalar `.x` is a `Point` property. Use `line.coords.x` for its X column.

### Equality and hashing

Geometries are **value objects**: `==` and `hash()` compare the CRS, coordinate
epoch, and exact geometry (type, coordinates including Z/M, and vertex order), so
geometries work in `set`s and as `dict` keys:

```python
import gometry as gm

a = gm.LineString([(0, 0), (1, 1)])
b = gm.LineString([(0, 0), (1, 1)])
reversed_ = gm.LineString([(1, 1), (0, 0)])
print("a == b:", a == b, "| a == reversed:", a == reversed_)
print("distinct:", len({a, b, reversed_}))
```

```text
a == b: True | a == reversed: False
distinct: 2
```

This `==` is **structural** (vertex-order sensitive). For the topological
"same shape on the map" test that ignores vertex order, use
[`gm.equals`](https://gometry.monicz.dev/api/toplevel/predicates/#gometry.equals "equals"); for a representation-independent vertex check,
apply [`normalize`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.normalize "normalize") to both inputs before calling
[`gm.equals_exact`](https://gometry.monicz.dev/api/toplevel/predicates/#gometry.equals_exact "equals_exact").

## Dimensions: XY, XYZ, XYM, XYZM

gometry supports four coordinate layouts — **XY, XYZ, XYM, and XYZM** — from the
constructors through IO.

### Three meanings of "dimension"

"Dimension" has three distinct meanings in GIS, and gometry keeps them separate:

| Concept | Property | Example value |
| --- | --- | --- |
| **Topological dimension** | [`geom.topological_dimension`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.topological_dimension "topological_dimension property") | `0` (Point), `1` (LineString), `2` (Polygon) |
| **Coordinate axes / ordinate layout** | [`geom.coordinate_axes`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.coordinate_axes "coordinate_axes property") | `"XY"`, `"XYZ"`, `"XYM"`, `"XYZM"` |
| **Has-ordinate flags** | [`geom.has_z`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.has_z "has_z property"), [`geom.has_m`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.has_m "has_m property") | `True` / `False` |

```python
import gometry as gm

trace = gm.LineString(
    [(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)],
    z=[10.0, 20.0, 30.0],
    m=[0.0, 1.0, 2.0],
    crs=4326,
)
print("geometry_type:", trace.geometry_type)
print("topological_dimension:", trace.topological_dimension)
print("coordinate_axes:", trace.coordinate_axes)
print("has_z / has_m:", trace.has_z, "/", trace.has_m)
```

```text
geometry_type: LineString
topological_dimension: 1
coordinate_axes: XYZM
has_z / has_m: True / True
```

### Z is a spatial axis; M is a measure

- **Z** is an additional *spatial* ordinate, usually elevation or height. gometry does **not**
  assume Z is in meters unless a vertical CRS says so — Z is a third spatial number
  until a CRS gives it units.
- **M** is a *measure* ordinate. It commonly carries linear-referencing values, route
  measures, distance-along-line, per-vertex timestamps, or application-specific event
  alignment. **M is not a spatial axis** and must never be silently treated as Z. M is also
  distinct from the [coordinate epoch](https://gometry.monicz.dev/guide/crs/#coordinate-epochs) ([`geom.epoch`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.epoch "epoch property")): the
  epoch is a single decimal year that dates the whole geometry's CRS realization, whereas M is
  a free per-vertex value with no datum.

A GPX track or vehicle trace has both elevation (Z) and a
capture timestamp or route measure (M) aligned to each vertex. gometry lets you store both on
the geometry instead of the common workaround of a 2D line plus side arrays:

```python
import gometry as gm

track = gm.LineString(
    [(21.00, 52.00), (21.01, 52.01), (21.02, 52.00)],
    z=[105.0, 118.0, 110.0],
    m=[1_700_000_000, 1_700_000_030, 1_700_000_060],
    crs=4326,
)
coords = list(track.coords.select('XYZM'))
print("coordinate_axes:", track.coordinate_axes)
for x, y, z, m in coords:
    print(f"  lon={x} lat={y} elev={z} t={int(m)}")
```

```text
coordinate_axes: XYZM
  lon=21.0 lat=52.0 elev=105.0 t=1700000000
  lon=21.01 lat=52.01 elev=118.0 t=1700000030
  lon=21.02 lat=52.0 elev=110.0 t=1700000060
```

[`coords.select(...)`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Coordinates.select "select") chooses a fixed coordinate-view
layout for nested output, iteration, column access, ndarray/dict exports, and
equality without changing the geometry. Nested coordinate export preserves
missing geometry rows as `None`; flattened columns omit their coordinates.

### Explicit constructors identify ambiguous tuples

A bare 3-tuple `(x, y, v)` is ambiguous — it could be XYZ or XYM. Use the
`z=` and `m=` keywords to identify the layout:

```python
import gometry as gm

p_xy   = gm.Point(21.0, 52.0, crs=4326)
p_xyz  = gm.Point(21.0, 52.0, z=105.0, crs=4326)
p_xym  = gm.Point(21.0, 52.0, m=1_700_000_000.0, crs=4326)
p_xyzm = gm.Point(21.0, 52.0, z=105.0, m=1_700_000_000.0, crs=4326)

for p in (p_xy, p_xyz, p_xym, p_xyzm):
    print(p.coordinate_axes, "→", p.to_wkt())
```

```text
XY → POINT (21 52)
XYZ → POINT Z (21 52 105)
XYM → POINT M (21 52 1700000000)
XYZM → POINT ZM (21 52 105 1700000000)
```

For arrays and lines, pass `z=` and `m=` keywords to [gm.points](https://gometry.monicz.dev/api/toplevel/constructors-bulk-factories/#gometry.points "points") and [gm.LineString](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.LineString "LineString"):

partial: requires coordinate arrays from the surrounding application

```python
points_xy = gm.points(lons, lats, crs=4326)
points_zm = gm.points(lons, lats, z=elevations, m=timestamps, crs=4326)
line      = gm.LineString(xy, z=elevations, m=timestamps, crs=4326)
```

Consistent layout per scalar geometry

A non-collection geometry must use one coordinate-axis layout for every vertex — a
[`LineString`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.LineString "LineString") cannot mix XY and XYZM vertices. A [`GeometryCollection`](https://gometry.monicz.dev/api/geometry/geometry-types/#gometry.GeometryCollection "GeometryCollection") may hold children
with different layouts, but the API makes that explicit rather than silent.

WKT carries the `Z`, `M`, and `ZM` dimensional tags, which round-trip through gometry:

```python
import gometry as gm

print(gm.from_wkt("POINT Z (1 2 3)").coordinate_axes)
print(gm.from_wkt("POINT M (1 2 7.5)").coordinate_axes)
print(gm.from_wkt("POINT ZM (1 2 3 7.5)").coordinate_axes)
```

```text
XYZ
XYM
XYZM
```

## Measuring with Z and M

**3D spatial measurement** uses the Z axis. [`length_3d`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.length_3d "length_3d property") and point-to-point
[`distance_3d(other)`](https://gometry.monicz.dev/api/toplevel/measurement/#gometry.distance_3d "distance_3d") include the vertical component, and [`min_z`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.min_z "min_z property") / [`max_z`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.max_z "max_z property")
(properties) summarize the Z extent that complements the XY [`bounds`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.bounds "bounds property"). Because a 3D length only makes
sense when every axis shares one linear unit, these require a **projected** CRS (or no
CRS); a geographic CRS mixes degrees and metre heights and raises under every `unit=` —
reproject first.

They report the CRS's own linear unit, exactly like their 2D siblings: a US-survey-foot
CRS gives feet from `length_3d`, as it does from `length`. The free functions
[`gm.length_3d`](https://gometry.monicz.dev/api/toplevel/measurement/#gometry.length_3d "length_3d") and [`gm.distance_3d`](https://gometry.monicz.dev/api/toplevel/measurement/#gometry.distance_3d "distance_3d") take the
same `unit=` override as [`gm.length`](https://gometry.monicz.dev/api/toplevel/measurement/#gometry.length "length") and
[`gm.distance`](https://gometry.monicz.dev/api/toplevel/measurement/#gometry.distance "distance") when you want SI metres, projected native linear
units, or CRS-free raw coordinate units.

On a geometry without Z, accessors and metrics have different results. The
**accessors** (`min_z` / `max_z`) describe what is
there, returning `None` for a scalar or `nan` for an array element. The **metrics**
(`length_3d` / `distance_3d`) require Z to compute: a scalar **raises**, while an array
**degrades per element to `nan`** for the rows that lack Z.

```python
import gometry as gm
climb = gm.LineString([(0.0, 0.0), (3.0, 4.0), (3.0, 4.0)], z=[0.0, 0.0, 10.0], crs=32634)
print('length_3d:', climb.length_3d)
print('z extent:', climb.min_z, climb.max_z)
a = gm.Point(0.0, 0.0, z=0.0, crs=32634)
b = gm.Point(3.0, 4.0, z=12.0, crs=32634)
print('distance_3d:', gm.distance_3d(a, b))
```

```text
length_3d: 15.0
z extent: 0.0 10.0
distance_3d: 13.0
```

## Linear referencing

Linear referencing uses the geometry's CRS metric by default. Values are absolute
distances unless `normalized=True`, when `0.0` and `1.0` mean the start and end.
Pass `basis='m'` when the line's M ordinate is the route measure instead.

| Task | Distance basis (default) | M basis |
| --- | --- | --- |
| point at a location | [`line_interpolate(at)`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.line_interpolate "line_interpolate") | `line_interpolate(at, basis='m')` |
| location nearest a point | [`line_locate(point)`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.line_locate "line_locate") | `line_locate(point, basis='m')` |
| portion between two locations | [`line_substring(start, end)`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.line_substring "line_substring") | `line_substring(start, end, basis='m')` |

```python
import gometry as gm

line = gm.LineString([(0.0, 0.0), (10.0, 0.0)], crs=32634)
midpoint = line.line_interpolate(0.5, normalized=True)
samples = line.line_interpolate(count=3)
print('midpoint:', midpoint.to_wkt())
print('samples:', [point.to_wkt() for point in samples])
```

```text
midpoint: POINT (5 0)
samples: ['POINT (0 0)', 'POINT (5 0)', 'POINT (10 0)']
```

### Distances and route positions

With CRS-aware units (the default on a geographic CRS), linework edges are
straight in the CRS's longitude/latitude chart and LRS measures the ellipsoidal
arclength of the retained chart-straight source edge, rather than geodesic
distance between each pair of vertices.

`line_locate` minimizes against those
same chart edges. When backend distances compare equal, it returns the interior
projection before a point or endpoint, then uses route order. This ranks
available measurements, not an exact geodesic continuum minimum. Repeated
support and self-crossings can have several stations at one location, so
locate/interpolate is not a unique inverse there.

An independently interpolated point has canonical longitude, while a
`line_substring` cut stays in the retained source edge's lifted chart.

`unit='planar'` instead uses Cartesian distance in the stored coordinate space.
Planar distance-basis locate uses the exactly nearest segment; exact ties prefer
an interior projection over an endpoint, then the first segment in route order,
matching planar proximity.

Zero-length X/Y segments consume no distance, but their stored vertices remain
on the route.

Interpolation at an interior prefix shared by a zero-length run
selects its first stored vertex; normalized `0.0` and `1.0` select the first
and last stored vertices, including when the whole route has zero length.
`line_substring` includes active-distinct stored vertices in its inclusive
range, even when consecutive vertices share X/Y and differ only in Z/M.
At a multipart prefix boundary, interpolation selects the preceding part's
endpoint, so interpolating a locate station at that boundary need not recover
the next part's nearest point.

### Sampling multiple locations

On the distance basis, `line_interpolate` accepts a sequence of locations or
`count=` for evenly spaced samples. With `basis='m'`, a scalar line accepts one
stored measure for a `Point` or a measure sequence for a `GeometryArray[Point]`;
an array receiver accepts one measure per line row.

An array receiver returns
row-aligned points for one location per row or [`Groups`](https://gometry.monicz.dev/api/geometry/coordinates/#gometry.Groups "Groups") when
each row has its own sample count or explicit station sequence.

Construct
numeric groups from a flat NumPy payload and CSR offsets, for example
`gm.Groups(np.array([0, 5, 10, 2, 8]), [0, 3, 5])`, and pass them as `at`;
each route uses its corresponding stations, under either distance or M basis.

Plain numeric `at` sequences remain flat, including `[]`; use `Groups` to request
grouped output even for zero routes.
An empty station row produces an empty group, a missing station row or route
produces a missing group, and an empty route produces one empty point per
station.

`normalized` and `unit` belong only to the
distance basis, so a measured route never silently mixes two coordinate
systems. On the distance basis, `unit` selects the route metric even when
`normalized=True`:
fractions are measured along that selected route, so changing `unit` can change
the resulting position.

`count=` also uses fractions of the selected metric and
accepts `unit=`, but rejects `normalized=True` because the count already defines
fractional sample positions.

### M-based positions

M linear referencing locates positions by the M ordinate rather than by
fractional or absolute length. `line_interpolate(m, basis='m')` returns the
point at a given measure, `line_locate(point, basis='m')` returns the measure
nearest a point, and `line_substring(start_m, end_m, basis='m')` cuts the
section between two measures. They require every vertex to carry a monotonic,
non-decreasing M. The extent accessors mirror the Z family:
[`min_m`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.min_m "min_m property") / [`max_m`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.max_m "max_m property") summarize the measure span (`None` for a scalar, `nan` for an
array element, when no vertex carries M).

Within each edge, M varies linearly with arclength. For increasing endpoint
measures, a requested `m` selects the arclength fraction
`(m - m0) / (m1 - m0)`; equal endpoint measures remain constant.
Z is carried at that same fraction, and `line_locate(..., basis='m')`
converts the nearest point's arclength fraction back to M.

On a geographic
CRS this is ellipsoidal arclength of the retained chart-straight source edge;
the fraction is generally different from its longitude/latitude parameter.
For example, a WGS 84 meridian from latitude −10° to 10° with endpoint
measures 100 and 300 places M=150 at latitude approximately
−5.00037949475414°, not −5°.

Distance-basis interpolation and substring use
the same Z/M carry rule; `interpolate_m` assigns vertex measures by cumulative
edge arclength without moving X/Y/Z.

### Empty routes and invalid measures

Empty linework has no positions, so every
form answers it instead of raising: `line_locate` returns `nan`,
`line_interpolate` returns `POINT EMPTY`, `line_substring` returns
`LINESTRING EMPTY`, and `interpolate_m` returns the empty geometry with an M
axis. A `POINT EMPTY` query locates to `nan`.

Missing or non-monotonic M is
different: both scalar and array calls raise
[`InvalidGeometryError`](https://gometry.monicz.dev/api/errors/#gometry.InvalidGeometryError "InvalidGeometryError"), because they asked for
measures the geometry does not have. Array failures identify the original input
row; an invalid route is never converted into a successful empty result.
Wrong-kind and parameter errors also raise on both.

```python
import gometry as gm

route = gm.LineString([(0.0, 0.0), (10.0, 0.0)], m=[0.0, 100.0], crs=32634)
print("at m=50:", route.line_interpolate(50.0, basis='m'))
print("locate:", route.line_locate(gm.Point(5.0, 0.0, crs=32634), basis='m'))
print("m 20..80:", route.line_substring(20.0, 80.0, basis='m'))
```

```text
at m=50: POINT M (5 0 50)
locate: 50.0
m 20..80: LINESTRING M (2 0 20, 8 0 80)
```

## Working with coordinate sequences

[`segments()`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.segments "segments") extracts each consecutive pair of
stored vertices as a `LineString`, including closing ring edges and repeated
coordinate pairs. It visits lines and polygon rings independently, so parts
and holes never acquire connecting edges. Points and empty geometries yield
no segments. A scalar returns a `GeometryArray[LineString]`; an array returns
one segment group per source row, retaining missing rows.

[`line_merge(directed=True)`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.line_merge "line_merge") joins line endpoints
without reversing any source line. Both directed and ordinary merging stop
at branching junctions. Directed merging also stops wherever a join would
require reversing a source sequence.
When joined endpoints share X/Y but differ in Z or M, directed merging retains
both stored occurrences as consecutive vertices.
Geographic endpoints also join across seam and pole aliases. Their published
longitude spellings may change, with vertices inserted where needed to preserve
an authored full-period edge. Inserted Z/M follow the original source arc.

Use [`map_coordinates`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.map_coordinates "map_coordinates") for one flattened,
same-shape coordinate transform. Use
[`map_coordinate_sequences`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.map_coordinate_sequences "map_coordinate_sequences") when
lines and rings need independent edits or a different vertex count. Its callback
receives a read-only `(N, D)` float64 array in each sequence's native ordinate
schema. Return a matrix with any number of rows and the same `D`.

```python
import gometry as gm

import numpy as np

line = gm.LineString([(0, 0), (2, 1), (4, 0)])
print("segments:", line.segments().to_wkt())
print("endpoints:", line.map_coordinate_sequences(lambda c: c[[0, -1]]).to_wkt())
routes = gm.GeometryArray([line, line])
stations = gm.Groups(np.array([0, 1, 0.25, 0.75]), [0, 2, 4])
samples = routes.line_interpolate(stations, normalized=True)
print("sample counts:", samples.counts.tolist())
```

```text
segments: ['LINESTRING (0 0, 2 1)', 'LINESTRING (2 1, 4 0)']
endpoints: LINESTRING (0 0, 4 0)
sample counts: [2, 2]
```

Parts and rings keep their grouping, coordinate schemas, CRS, and epoch.
Points receive one-coordinate matrices and can return zero or one coordinate.
A multipoint receives its stored point sequence; resizing it replaces its point
membership, including empty slots. Empty source components skip the callback. Empty output
empties a point or line and removes a hole; a polygon becomes empty when all its
rings become empty. An empty shell with nonempty holes is an error.

Callback output uses ordinary constructor admission: a nonempty line needs at
least two vertices, and a nonempty ring needs three open corners or four closed
vertices. Open rings are closed on all active ordinates; conflicting Z/M closure
is rejected. Non-finite coordinates and changed ordinate widths raise errors.
No validity repair is performed.

## Z/M under operations

Planar Simple Features predicates are **XY topology** predicates. Whether `a` contains `b` is
decided in X/Y; Z and M are ignored for the truth value but preserved on the geometry.

Constructive operations resolve `Z` and `M` independently wherever they are
derivable and drop only the axis that cannot be sourced:

- Operations whose output vertices are **copied from input vertices** preserve Z/M:
  [`simplify`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.simplify "simplify"), [`concave_hull`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.concave_hull "concave_hull"),
  [`reverse`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.reverse "reverse"), the triangulations, and source vertices
  retained by [`convex_hull`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.convex_hull "convex_hull") keep their ordinates.
  The convex hull's XY shape is independent of input point order (±0.0 compares
  as the same ordinate). When several input vertices share an XY position, a
  copied hull vertex comes from the first vertex in storage order, including its
  Z/M and signed-zero spelling.
- Operations whose new vertices are **resolvable against the input** — overlay,
  [`split`](https://gometry.monicz.dev/api/toplevel/constructive-validation/#gometry.split "split"),
  [`snap`](https://gometry.monicz.dev/api/toplevel/constructive-validation/#gometry.snap "snap"), and linear referencing — interpolate Z/M along source
  segments. They retain active-distinct source vertices even when consecutive
  X/Y positions match; distance-based linear referencing gives zero-length X/Y
  segments no length.
  If an axis has a genuinely unsourceable output vertex (such as a box corner
  entering the output), or conflicting source values, that axis is
  dropped while the other axis may remain.
- On geographic input, overlays and source-preserving linework — including
  [`boundary`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.boundary "boundary"), [`split`](https://gometry.monicz.dev/api/toplevel/constructive-validation/#gometry.split "split"),
  [`snap`](https://gometry.monicz.dev/api/toplevel/constructive-validation/#gometry.snap "snap"), the default chart path of
  [`segmentize`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.segmentize "segmentize"),
  [`convex_hull`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.convex_hull "convex_hull"),
  [`nearest_points`](https://gometry.monicz.dev/api/toplevel/measurement/#gometry.nearest_points "nearest_points"),
  [`shortest_line`](https://gometry.monicz.dev/api/toplevel/measurement/#gometry.shortest_line "shortest_line"),
  [`minimum_clearance_line`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.minimum_clearance_line "minimum_clearance_line"),
  [`subdivide`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.subdivide "subdivide"),
  [`line_merge`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.line_merge "line_merge"),
  [`coverage_union_all`](https://gometry.monicz.dev/api/toplevel/coverage-polygonal/#gometry.coverage_union_all "coverage_union_all"), and
  [`coverage_simplify`](https://gometry.monicz.dev/api/toplevel/coverage-polygonal/#gometry.coverage_simplify "coverage_simplify") carry eligible Z/M at an
  inserted vertex when its producer traces it to a source edge. Z/M follow
  that edge's original endpoints by ellipsoidal arclength of the retained
  chart-straight source edge on its CRS ellipsoid, not by its chart parameter
  or a later cut fragment. This includes vertices inserted at the antimeridian
  for GeoJSON serialization.

  Source vertices retain their stored ordinate bits; when sources disagree,
  Z and M resolve independently and only a conflicting or unavailable axis
  is dropped. A planar `unit=` choice for a nearest witness does not change
  how its geographic source-edge ordinates are carried.

  A computed point with no sourceable edge has no source Z/M. Recursive
  subdivision retains original input-edge traces through later cuts,
  including seam vertices (±180° in EPSG:4326).

  On a certified zero-length source edge, an interior sample retains an
  axis only when both endpoint values have identical bits; a disagreement
  follows the operation's per-axis conflict policy. Exact source endpoints
  keep their own values. A nonzero edge whose measured length underflows
  to zero raises a metric error.
- A degenerate [`minimum_rotated_rectangle`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.minimum_rotated_rectangle "minimum_rotated_rectangle")
  that follows geographic source-edge support carries resolvable Z/M by the
  same original-edge arclength rule. A computed areal rectangle is a new
  XY construction and is returned in XY.
- Empty overlay results retain the axes declared by the operation: `difference`
  follows its first operand, while the other set operations use the union of
  their operands' axes.
- Operations that **compute brand-new points** — [`centroid`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.centroid "centroid"), [`point_on_surface`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.point_on_surface "point_on_surface"), [`envelope`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.envelope "envelope"),
  [`buffer`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.buffer "buffer"), `voronoi_*`,
  [`polylabel`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.polylabel "polylabel") — cannot source Z/M for invented vertices, so they return 2D.
- Circle solutions from [`maximum_inscribed_circle`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.maximum_inscribed_circle "maximum_inscribed_circle")
  and [`minimum_bounding_circle`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.minimum_bounding_circle "minimum_bounding_circle") have
  XY computed centers and positive-radius polygon realizations. They preserve
  the source CRS and coordinate epoch. A zero-radius bounding center selected
  from an input site retains the first source point's Z/M and stored spelling.

```python
import gometry as gm

track = gm.LineString(
    [(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)],
    z=[10.0, 20.0, 30.0], m=[0.0, 1.0, 2.0], crs=4326,
)

centroid = track.centroid()
print("centroid:", centroid.geometry_type, centroid.coordinate_axes)

flat = track.force_2d()
print("force_2d:", flat.geometry_type, flat.coordinate_axes)
```

```text
centroid: Point XY
force_2d: LineString XY
```

XYZM survey track

The survey track has three XY vertices carrying Z and M ordinates.

2D centroid

The computed centroid is a new 2D point, so Z and M are absent.

- input
- result

For a permanent 2D conversion, use [`force_2d`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.force_2d "force_2d") to
drop Z and M. [`force_3d`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.force_3d "force_3d") fills only vertices that
lack Z. Use [`with_coordinates`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.with_coordinates "with_coordinates") to replace
one or more ordinates without changing topology. Each supplied value can be a
scalar broadcast over vertices or a column with one value per vertex in the
flattened coordinate sequence; omitted ordinates are preserved, and
`z=None` / `m=None` removes that optional axis. X and Y cannot be removed.
Matrix mode replaces the coordinate matrix using its existing axis layout and
cannot be mixed with ordinate columns:

```python
import gometry as gm

track = gm.LineString([(0, 0), (1, 1), (2, 0)], z=[10, 20, 30], crs=4326)
flat = track.force_2d()
print("force_2d axes:", flat.coordinate_axes)
print("flat centroid:", (flat.centroid()).to_wkt())
print("force_3d axes:", flat.force_3d().coordinate_axes)
print("with_coordinates(z=...):", flat.with_coordinates(z=10.0))
print("with_coordinates(m=...):", flat.with_coordinates(m=0.0).coordinate_axes)
```

```text
force_2d axes: XY
flat centroid: POINT (1 0.5)
force_3d axes: XYZ
with_coordinates(z=...): LINESTRING Z (0 0 10, 1 1 10, 2 0 10)
with_coordinates(m=...): XYM
```

## IO and format limits

WKT, WKB/EWKB, and GeoArrow preserve both ordinates. [`to_geojson()`](https://gometry.monicz.dev/api/geometry/geometry/#gometry.Geometry.to_geojson "to_geojson") raises on M, and `__geo_interface__` raises on M or a
coordinate epoch because GeoJSON has no slots for them. WKB/EWKB preserve M but
not epoch; GeoArrow preserves epoch metadata. WKT/WKB preserve dimensional-empty
axes, while GeoJSON does not. Format details are in [Text & binary
formats](https://gometry.monicz.dev/ecosystem/text-formats/); GeoArrow's columnar metadata is described
in [Arrow & storage](https://gometry.monicz.dev/ecosystem/arrow/).

## Enforcing a dimension contract

Use `axes=` with [gm.require](https://gometry.monicz.dev/api/toplevel/constructive-validation/#gometry.require "require") to require a coordinate layout.
For example, require XY when your storage must not carry Z or M:

partial: requires an input object from the surrounding application

```python
area = gm.require(obj, crs=4326, axes="XY")

trace = gm.require(obj, crs=4326, axes="XYZM")
```

See [Validation](https://gometry.monicz.dev/guide/validation/) for topology checks and ingestion examples.

## See also

- [Predicates](https://gometry.monicz.dev/guide/predicates/) — spatial relationships ([`contains`](https://gometry.monicz.dev/api/toplevel/predicates/#gometry.contains "contains"), [`intersects`](https://gometry.monicz.dev/api/toplevel/predicates/#gometry.intersects "intersects"), [DE-9IM](https://en.wikipedia.org/wiki/DE-9IM)).
- [Constructive operations](https://gometry.monicz.dev/guide/constructive/) — buffers, overlays, hulls, and how they carry Z/M.
- [CRS](https://gometry.monicz.dev/guide/crs/) — the metric frame for lon/lat area, length, and distance.
- [Arrays](https://gometry.monicz.dev/guide/arrays/) — vectorized [`GeometryArray`](https://gometry.monicz.dev/api/geometry/geometryarray/#gometry.GeometryArray "GeometryArray") columns and the missing-row rule.
- [Validation](https://gometry.monicz.dev/guide/validation/) — the OGC validity side of the storage contract.
