Skip to content

Geospatial

Location-based patterns

Once you can store geometry types and run spatial queries with distance in mind, most product features follow a small set of patterns.

Store each site's coordinates as a Point, optionally with a category and opening hours. At query time, supply the user's location, compute distance or bounding-box filters, and sort by proximity. From SurrealDB 4.0, a SPATIAL index on the location field answers INSIDE, INTERSECTS and geo::distance(location, $here) < $radius from the index, without scanning the whole table. Before 4.0, store a geohash (from geo::hash::encode()) in a field with a standard index, or at the start of the record ID.

A geohash in the record ID is still useful when every query reads one cell of the map. For example, consider records used to store millions of events by location. These can use array-based record IDs that start with a geohash, so a query can select every record with exactly this hash without any further filtering.

DEFINE FUNCTION fn::create_geo_record($point: point) -> object {
    LET $hash = geo::hash::encode($point, 4);
    CREATE ONLY event:[$hash, $point, rand::ulid()];
};

fn::create_geo_record((50.0, 50.1));
fn::create_geo_record((50.1, 50.1));
fn::create_geo_record((50.5, 50.1));

SELECT * FROM event:['v0gt', NONE, NONE]..['v0gt', .., ..];
Output: returns two of three above events
[
	{
		id: event:[
			'v0gt',
			(50f, 50.1f),
			'01KNTYS85HQZ7T4D066MAVA3G9'
		]
	},
	{
		id: event:[
			'v0gt',
			(50.1f, 50.1f),
			'01KNTYS85H8JHM3T9YKGMT2BV8'
		]
	}
]

The third event is about 30 km east of the other two, in the neighbouring cell 'v0gv', so the range does not reach it:

A three-by-three grid of precision-4 geohash cells with 'v0gt' in the centre. Events 1 and 2 at (50.0, 50.1) and (50.1, 50.1) are inside 'v0gt', just above its southern edge at latitude 50.098. Event 3 at (50.5, 50.1) is in the cell to the east, 'v0gv'. A side panel shows the prefixes 'v', 'v0', 'v0g' and 'v0gt' narrowing the cell at precisions 1 to 4.A three-by-three grid of precision-4 geohash cells with 'v0gt' in the centre. Events 1 and 2 at (50.0, 50.1) and (50.1, 50.1) are inside 'v0gt', just above its southern edge at latitude 50.098. Event 3 at (50.5, 50.1) is in the cell to the east, 'v0gv'. A side panel shows the prefixes 'v', 'v0', 'v0g' and 'v0gt' narrowing the cell at precisions 1 to 4.

Represent coverage as polygons (or multipolygons for islands). New customers or orders are classified with point-in-polygon tests.

For a single zone record, store the boundary as a polygon geometry and test whether a customer’s location falls inside it using CONTAINS (polygon on the left, point on the right):

DEFINE TABLE service_area SCHEMAFULL;
DEFINE FIELD name ON service_area TYPE string;
DEFINE FIELD boundary ON service_area TYPE geometry<polygon>;

CREATE service_area:greater_london SET
	name = "Greater London",
	boundary = {
		type: "Polygon",
		coordinates: [[
			[-0.38314819, 51.37692386],
			[0.1785278, 51.37692386],
			[0.1785278, 51.61460570],
			[-0.38314819, 51.61460570],
			[-0.38314819, 51.37692386]
		]]
	};

LET $customer = (-0.118092, 51.509865);
SELECT name FROM service_area WHERE boundary CONTAINS $customer;
Output
[
	{
		name: 'Greater London'
	}
]

A point outside the ring (for example further north) returns no records. For overlapping territories, you may return several matches and disambiguate with priority, postal rules, or extra fields on service_area.

For vehicles or assets reporting on a schedule, store recent positions as points with timestamps and use line strings or buffers when you care about adherence to a route corridor rather than a single snapshot.

Was this page helpful?