Geometry and geography
Documentation / Spatial and temporal values
Updated 20 September 2026.
Store shapes on nodes and relationships, test how they relate, or combine them
into new shapes using spatial.* functions. Both geometry and geography support
POINT, LINESTRING, POLYGON (with holes), MULTIPOINT, MULTILINESTRING, MULTIPOLYGON
and GEOMETRYCOLLECTION. Values are stored as validated WKB.
Coordinates use 64-bit IEEE-754 floating-point numbers in memory and WKB, including Z when present. The SRID identifier is a 32-bit integer.
Start with a shape
CREATE (:Region {
name:'Central',
shape:spatial.fromWKT('POLYGON((0 0,4 0,4 4,0 4,0 0))')
});
Run the next example as a separate statement:
MATCH (r:Region)
WHERE spatial.covers(r.shape, spatial.fromWKT('POINT(2 2)'))
RETURN r.name, spatial.asWKT(spatial.centroid(r.shape));
This returns Central and POINT (2 2). covers includes the boundary;
contains excludes boundary points when testing a point against a polygon.
Geometry, geography and SRID
Geometry uses straight segments in coordinate space. Geography uses minor great-circle arcs on a sphere. Choose the domain explicitly for longitude/latitude calculations; an SRID alone does not select spherical operations.
| Domain | XY | XYZ |
|---|---|---|
| Geometry defaults | 7203, Cartesian x/y | 9157, Cartesian x/y/z |
| Geography defaults | 4326, longitude/latitude | 4979, longitude/latitude/height |
| Geometry can also use | 4326 | 4979 |
Coordinates are always longitude, latitude, in that order, for geographic SRIDs. Longitude must be between -180 and 180; latitude between -90 and 90. Changing an SRID does not reproject coordinates. Unknown SRIDs and conflicting embedded SRIDs are errors. Binary operations require matching domain, SRID and coordinate layout.
WITH spatial.fromWKT(
'POLYGON((170 -10,-170 -10,-170 10,170 10,170 -10))',
{domain:'geography', srid:4326}
) AS region
RETURN spatial.contains(region,
spatial.fromWKT('POINT(179 0)', {domain:'geography'}));
This is true: the region crosses the antimeridian. Geography is spherical,
not ellipsoidal. Lines and polygons generally need a common open hemisphere
for operations; unsupported global extents return an error.
Existing point values
Legacy point(), its fields, distances and POINT indexes
continue to work. Convert explicitly before passing a legacy point to a shape
operation:
WITH point({longitude:-0.12, latitude:51.5}) AS p
RETURN spatial.asWKT(spatial.fromPoint(p, {domain:'geography'}));
spatial.toPoint(shape) converts a nonempty spatial POINT back to a legacy point,
preserving its SRID. fromPoint validates coordinates and cannot relabel the CRS.
Predicates and overlays
| Function | Meaning |
|---|---|
spatial.intersects(a,b) / disjoint(a,b) | Any shared space / no shared space |
spatial.touches(a,b) | Boundary contact without shared interiors |
spatial.overlaps(a,b) | Same-dimensional partial overlap, neither covering the other |
spatial.contains(a,b) / within(a,b) | Interior containment / its reverse |
spatial.covers(a,b) / coveredBy(a,b) | Containment including boundary-only cases |
spatial.crosses(a,b) | Crossing interiors, such as two crossing lines |
spatial.equals(a,b) | Same occupied shape |
spatial.relate(a,b[, pattern]) | Planar DE-9IM string or boolean pattern test |
spatial.union(a,b) | Coverage from either shape |
spatial.intersection(a,b) | Shared coverage |
spatial.difference(a,b) | A with B subtracted |
spatial.symDifference(a,b) | Coverage from either shape, excluding their intersection |
spatial.clip(a,mask) | Intersection with a polygonal mask |
spatial.identity(a,mask) | Map containing inside and outside shapes |
Overlay can return a line or point when polygons meet only along an edge or corner. Results are closed shapes: subtracting a point cannot create an open puncture in a line. Identity partitions geometry; retain feature attributes explicitly in your Cypher projection.
= compares representation, including coordinate order, SRID and domain.
Use spatial.equals for topological equality. Null data propagates. Typed input
empties keep their type; computed empty overlays are GEOMETRYCOLLECTION EMPTY.
Centroid
spatial.centroid(shape) returns a spatial POINT with the same domain and SRID.
It weights points equally, lines by length and polygons by area, subtracting
holes. For mixed collections, only the highest nonempty dimension contributes.
Geographic centroids use spherical first moments. Overlapping collection members
contribute as supplied; dissolve them first if you want the centre of their union.
A centroid can lie outside a concave polygon or inside a hole. Empty input gives
POINT EMPTY; a spherical input with no unique centroid direction returns an error.
Formats, validation and drivers
| Import | Export |
|---|---|
spatial.fromWKT(text[, options]) | asWKT, asEWKT |
spatial.fromWKB(bytes[, options]) | asWKB, asEWKB |
spatial.fromWKBHex(hex[, options]) | asWKBHex |
spatial.fromGeoJSON(mapOrText[, options]) | asGeoJSON (returns a map) |
spatial.fromMap(map) | asMap |
Constructor options include domain, srid and model. Unknown options fail.
WKT/WKB accept XY and Z, typed empties, nested collections, embedded SRIDs and
both byte orders. Plain WKB carries neither SRID nor domain; EWKB carries SRID,
but you must still retain the domain separately.
All import paths validate structure, finite coordinates, layout, CRS and topology.
Malformed text, trailing bytes, self-crossing polygon rings, holes outside shells
and overlapping MultiPolygon components raise errors. There is no permissive
import switch or automatic repair. spatial.isValid is true for every successfully
constructed shape. spatial.validation returns the corresponding diagnostic map.
GeoJSON accepts RFC 7946 geometry objects in SRID 4326/4979. It rejects Features, custom CRS members, duplicate JSON keys, nonfinite numbers, mixed layouts and incorrect bounding boxes. Foreign JSON members are ignored. Empty primitive coordinate arrays and empty children inside multipart coordinate arrays are rejected; use WKT/WKB for those. Empty multipart arrays and GeometryCollections work.
RETURN apoc.convert.toJson(spatial.asGeoJSON(
spatial.fromGeoJSON({type:'Point', coordinates:[-0.12,51.5]})
));
Geography import requires {domain:'geography', edges:'greatCircle'} to acknowledge
the change from coordinate-space segments to spherical arcs. Geography POINT and
MULTIPOINT export directly; lines/polygons need an explicit approximation budget:
RETURN spatial.asGeoJSON(
spatial.fromWKT('LINESTRING(170 20,-170 20)', {domain:'geography'}),
{maxDeviationMetres:200000}
);
Export subdivides arcs and cuts the antimeridian. The conservative spherical bound
uses radius 6,378,137 m and at most 4096 output positions. A tighter budget can
return Spatial.LimitExceeded. Polar loops cannot currently be exported to
GeoJSON. Cartesian SRIDs 7203/9157 also cannot be exported as GeoJSON. Use WKT/WKB
for lossless interchange; GeoJSON is not a lossless geography round-trip.
On the wire, spatial values use a map envelope containing $gdbType:'spatial',
version:1, domain, srid, layout, model and wkb bytes. The seven native
drivers decode this envelope into a named Spatial object and encode it again
when bound as a parameter. WKB bytes and metadata are preserved, including
geometry/geography, XY/XYZ, nested collections and typed empty shapes.
The server does not automatically promote parameter maps to spatial values.
Use spatial.fromMap($shape) when storing or operating on a bound Spatial.
RETURN $shape only echoes the envelope. General JSON export uses wkbHex in
the same envelope; fromMap accepts that representation too.
This Python example uses the native driver:
import os
from galactus import Driver, Spatial
with Driver("bolt://127.0.0.1:7687", "gdb", os.environ["GDB_PASSWORD"]) as driver:
shape = driver.execute_query(
"RETURN spatial.fromWKT($wkt, {domain:$domain}) AS shape",
{"wkt": "POLYGON((0 0,4 0,4 4,0 4,0 0))", "domain": "geometry"},
).records[0]["shape"]
assert isinstance(shape, Spatial)
stored = driver.execute_query(
"CREATE (r:Region {shape:spatial.fromMap($shape)}) RETURN r.shape AS shape",
{"shape": shape},
).records[0]["shape"]
assert stored.wkb == shape.wkb
assert (stored.domain, stored.srid, stored.layout, stored.model) == (
shape.domain, shape.srid, shape.layout, shape.model
)
See each language Quickstart for its
native type mapping and the separate Point2D / Point3D types.
Indexes and inspection
Spatial shape indexes use
CREATE SPATIAL INDEX and CALL db.index.spatial.intersects(...) YIELD node.
Cartesian envelopes prune candidates before the shape predicate. Geographic
indexes currently refine a scan. A spatial WHERE clause does not automatically
select an index.
Inspect values with spatial.type, srid, crs, domain, model,
coordinateDimension, dimension, isEmpty and parts. spatial.capabilities()
reports the backend and limits.
Current limits and upgrades
XYZ is retained in storage and interchange; topology and centroid require XY.
Use spatial.force2D(shape) explicitly to drop Z. M/ZM, curves, solids, arbitrary
EPSG definitions, reprojection and precision-grid options are unsupported.
The format budget is 16 MiB with nesting depth 32. The topology kernel has a stricter 4096 combined point/segment budget and bounded work/output limits. Planar nonzero XY ordinates must be between 1e-100 and 1e100 in magnitude. Numerical collapse or an unresolvable arrangement raises an error. Kernel cancellation is not yet supported.
Spatial values and index declarations persist across restarts and checkpoints. They require the new storage format: snapshots containing spatial data use version 4 and new commit segments use version 3. Older binaries reject these versions. Take a backup before upgrading and do not reopen an upgraded directory with an older image.
Related articles
Legacy points · Spatial indexes · Bolt clients · Compatibility