A new universe for your graph data.Meet Galactus DB
THE GALACTUS DB BLOG

Parks, ponds and paths: giving your graph a sense of place

20/09/2026

Published

A pin on a map tells you where something is. It is less helpful when the question is "Can we deliver there?", "Does this route cross the park?" or "Why have we put the meeting point in a pond?"

Galactus DB's spatial shapes let your graph work with areas and routes as well as points. You can keep them alongside the rest of your data and ask those questions in Cypher. Let's take a walk through a small imaginary park.

The pond is not part of the picnic area

Our park is a square with a pond in the middle. For this example, the pond is a hole in the usable park area. There is a cafe on dry land and a marker in the water. The coordinates use a simple local grid, so no map is needed.

Run each code block separately in a fresh database:

CREATE (:Park {
  name:'Riverside',
  shape:spatial.fromWKT(
    'POLYGON((0 0,10 0,10 10,0 10,0 0),(4 4,4 6,6 6,6 4,4 4))'
  )
}),
(:Place {name:'Cafe', shape:spatial.fromWKT('POINT(2 2)')}),
(:Place {name:'Pond marker', shape:spatial.fromWKT('POINT(5 5)')});

The polygon's first ring is its outside boundary; the second is the hole. Now we can ask which places sit in the usable area:

MATCH (park:Park), (place:Place)
WHERE spatial.covers(park.shape, place.shape)
RETURN place.name;

The answer is Cafe. The pond marker is inside the outer square, but the hole leaves it out. Geography has saved us from a rather damp lunch.

covers includes the boundary too. For a point strictly inside, use contains. That distinction matters for a delivery address right on the edge of a service area: decide whether the boundary belongs before choosing the function.

Keep the useful bit of a route

A maintenance route starts west of the park and finishes east of it. We only want the stretch inside:

MATCH (park:Park)
RETURN spatial.asWKT(spatial.clip(
  spatial.fromWKT('LINESTRING(-2 2,12 2)'),
  park.shape
)) AS routeInside;

The result runs from (0,2) to (10,2). clip cuts the route using the park as a mask and returns a shape you can store or use in another query.

If you only need a yes or no, intersects answers whether shapes meet. touches, within and crosses help you ask more specific questions: a route passing through an area is different from one that only brushes its boundary.

Sorry, this half is closed

Suppose maintenance closes the western half of the park. We want to keep both the closed area and the part still open:

MATCH (park:Park)
WITH spatial.identity(
  park.shape,
  spatial.fromWKT('POLYGON((0 0,5 0,5 10,0 10,0 0))')
) AS split
RETURN spatial.asWKT(split.inside) AS closed,
       spatial.asWKT(split.outside) AS stillOpen;

identity returns both pieces as inside and outside. Other operations let you choose just the result you need:

FunctionWhat you keep
spatial.union(a,b)Everything covered by either shape
spatial.intersection(a,b)Only their shared coverage
spatial.difference(a,b)A with B taken out
spatial.symDifference(a,b)Either shape, excluding the shared part

A shared part is not always an area. Two parks might meet along a fence or at a corner, giving you a line or a point. That is useful when your question is about a shared boundary rather than room for a picnic blanket.

The centre is in the pond. Of course it is.

MATCH (park:Park)
RETURN spatial.asWKT(spatial.centroid(park.shape)) AS centre;

Our symmetrical park returns POINT (5 5), right in the water. A centroid is a weighted centre, so it makes no promise that you can stand there. It is useful for analysis; give it a little thought before sending anyone to it.

Finding shapes as the graph grows

Shapes have their own spatial index, separate from the existing point index:

CREATE SPATIAL INDEX park_shapes FOR (park:Park) ON (park.shape);

Use the index procedure to find shapes that meet the route:

CALL db.index.spatial.intersects(
  'park_shapes',
  spatial.fromWKT('LINESTRING(-2 2,12 2)')
)
YIELD node
RETURN node.name;

This returns Riverside. For Cartesian geometry, the index checks bounding boxes to narrow the search, then checks the actual shapes. The pond still counts as a hole.

Call the procedure explicitly when you want the index; WHERE spatial.intersects(...) does not select it automatically. Geographic indexes currently refine a scan, so they do not offer the same bounding-box shortcut. Index declarations persist on a durable instance and stay in step with changes to the graph.

Taking the park out into the world

Local grids work well for floor plans and examples like this one. For longitude and latitude with great-circle edges, choose the geography domain. Coordinates are always longitude, latitude. Set the domain and SRID consistently: changing an SRID does not transform the coordinates for you.

You can bring shapes in and out using WKT, WKB and GeoJSON. Existing point() queries still work, and spatial.fromPoint lets you turn a point into a shape explicitly.

There are a few boundaries to keep in mind. Spatial calculations are 2D, even when a stored shape includes height. Geography uses a sphere and has limits on the extents it can process; unrestricted global overlays are not supported. Geographic GeoJSON export needs explicit approximation settings, and polar export is not available yet.

The spatial guide has the coordinate examples, format rules, operation limits and upgrade notes when you need them.

Your graph can now connect the people, places and routes with the areas they actually occupy. That leaves you free to ask the useful questions, including whether there is a dry place to meet for coffee.