ArangoDB v3.4 reached End of Life (EOL) and is no longer supported.
This documentation is outdated. Please see the most recent version here: Latest Docs
Geo-Spatial Indexes
ArangoDB features a Google S2 based geospatial index since version 3.4.0, which supersedes the previous geo index implementation. Indexing is supported for a subset of the GeoJSON geometry types as well as simple latitude longitude pairs.
AQL’s geospatial functions and GeoJSON constructors are described in Geo functions.
Using a Geo-Spatial Index
The geospatial index supports containment and intersection
queries for various geometric 2D shapes. You should be mainly using AQL queries
to perform these types of operations. The index can operate in two different
modes, depending on if you want to use the GeoJSON data-format or not. The modes
are mainly toggled by using the geoJson
field when creating the index.
This index assumes coordinates with the latitude between -90 and 90 degrees and the longitude between -180 and 180 degrees. A geo index will ignore all documents which do not fulfill these requirements.
GeoJSON Mode
To create an index in GeoJSON mode execute:
collection.ensureIndex({ type: "geo", fields: [ "geometry" ], geoJson:true })
This creates the index on all documents and uses geometry as the attributed field where the value is either a Geometry Object or a coordinate array. The array must contain at least two numeric values with longitude (first value) and the latitude (second value). This corresponds to the format described in RFC 7946 Position
All documents, which do not have the attribute path or have a non-conform value in it, are excluded from the index.
A geo index is implicitly sparse, and there is no way to control its sparsity. In case that the index was successfully created, an object with the index details, including the index-identifier, is returned.
Non-GeoJSON mode
This index mode exclusively supports indexing on coordinate arrays. Values that contain GeoJSON or other types of data will be ignored. In the non-GeoJSON mode the index can be created on one or two fields.
The following examples will work in the arangosh command shell.
To create a geo-spatial index on all documents using latitude and longitude as separate attribute paths, two paths need to be specified in the fields array:
collection.ensureIndex({ type: "geo", fields: [ "latitude", "longitude" ] })
The first field is always defined to be the latitude and the second is the
longitude. The geoJson
flag is implicitly false in this mode.
Alternatively you can specify only one field:
collection.ensureIndex({ type: "geo", fields: [ "location" ], geoJson:false })
It creates a geospatial index on all documents using location as the path to the coordinates. The value of the attribute has to be an array with at least two numeric values. The array must contain the latitude (first value) and the longitude (second value).
All documents, which do not have the attribute path(s) or have a non-conforming value in it, are excluded from the index.
A geo index is implicitly sparse, and there is no way to control its sparsity. In case that the index was successfully created, an object with the index details, including the index-identifier, is returned.
In case that the index was successfully created, an object with the index details, including the index-identifier, is returned.
Indexed GeoSpatial Queries
The geospatial index supports a variety of AQL queries, which can be built with the help
of the geo utility functions. There are three specific
geo functions that can be optimized, provided that they are used correctly:
GEO_DISTANCE, GEO_CONTAINS, GEO_INTERSECTS
. Additionally, there is a built-in support to optimize
the older geo functions DISTANCE
, NEAR
and WITHIN
(the last two only if they are
used in their 4 argument version, without distanceName).
When in doubt whether your query is being properly optimized, check the AQL explain output to check for index usage.
Query for Results near Origin (NEAR type query)
A basic example of a query for results near an origin point:
FOR x IN geo_collection
FILTER GEO_DISTANCE([@lng, @lat], x.geometry) <= 100000
RETURN x._key
The first parameter can be a GeoJSON object or a coordinate array in [longitude, latitude]
ordering.
The second parameter is the document field on which the index was created. The function
GEO_DISTANCE
always returns the distance in meters, so will receive results
up until 100km.
Query for Sorted Results near Origin (NEAR type query)
A basic example of a query for the 1000 nearest results to an origin point (ascending sorting):
FOR x IN geo_collection
SORT GEO_DISTANCE([@lng, @lat], x.geometry) ASC
LIMIT 1000
RETURN x._key
The first parameter can be a GeoJSON object or a coordinate array in [longitude, latitude]
ordering.
The second parameter is the documents field on which the index was created.
You may also get results farthest away (distance sorted in descending order):
FOR x IN geo_collection
SORT GEO_DISTANCE([@lng, @lat], x.geometry) DESC
LIMIT 1000
RETURN x._key
Query for Results within Distance
A query which returns documents at a distance of 1km or farther away, up to 100km from the origin. This will return the documents with a GeoJSON value that is located in the specified search annulus.
FOR x IN geo_collection
FILTER GEO_DISTANCE([@lng, @lat], x.geometry) <= 100000
FILTER GEO_DISTANCE([@lng, @lat], x.geometry) >= 1000
RETURN x
Query for Results contained in Polygon
A query which returns documents whose stored geometry is contained within a GeoJSON Polygon.
LET polygon = GEO_POLYGON([[[60,35],[50,5],[75,10],[70,35]]])
FOR x IN geo_collection
FILTER GEO_CONTAINS(polygon, x.geometry)
RETURN x
The first parameter of GEO_CONTAINS
must be a polygon. Other types are not valid.
The second parameter must contain the document field on which the index was created.
Query for Results Intersecting a Polygon
A query which returns documents with an intersection of their stored geometry and a GeoJSON Polygon.
LET polygon = GEO_POLYGON([[[60,35],[50,5],[75,10],[70,35]]])
FOR x IN geo_collection
FILTER GEO_INTERSECTS(polygon, x.geometry)
RETURN x
The first parameter of GEO_INTERSECTS
must be a polygon. Other types are not valid.
The second parameter must contain the document field on which the index was created.
GeoJSON
GeoJSON is a geospatial data format based on JSON. It defines several different types of JSON objects and the way in which they can be combined to represent data about geographic shapes on the earth surface. GeoJSON uses a geographic coordinate reference system, World Geodetic System 1984 (WGS 84), and units of decimal degrees.
Internally ArangoDB maps all coordinates onto a unit sphere. Distances are projected onto a sphere with the Earth’s Volumetric mean radius of 6371 km. ArangoDB implements a useful subset of the GeoJSON format (RFC 7946). We do not support Feature Objects or the GeometryCollection type. Supported geometry object types are:
- Point
- MultiPoint
- LineString
- MultiLineString
- Polygon
Point
A GeoJSON Point is a position comprised of a longitude and a latitude:
{
"type": "Point",
"coordinates": [100.0, 0.0]
}
MultiPoint
A GeoJSON MultiPoint is an array of positions:
{
"type": "MultiPoint",
"coordinates": [
[100.0, 0.0],
[101.0, 1.0]
]
}
LineString
A GeoJSON LineString is an array of two or more positions:
{
"type": "LineString",
"coordinates": [
[100.0, 0.0],
[101.0, 1.0]
]
}
MultiLineString
A GeoJSON MultiLineString is an array of LineString coordinate arrays:
{
"type": "MultiLineString",
"coordinates": [
[
[100.0, 0.0],
[101.0, 1.0]
],
[
[102.0, 2.0],
[103.0, 3.0]
]
]
}
Polygon
A GeoJSON Polygon consists
of a series of closed LineString
objects (ring-like). These Linear Ring objects
consist of four or more vertices with the first and last coordinate pairs
being equal. Coordinates of a Polygon are an array of linear ring coordinate
arrays. The first element in the array represents the exterior ring.
Any subsequent elements represent interior rings (holes within the surface).
- A linear ring may not be empty, it needs at least three distinct coordinates
- Within the same linear ring consecutive coordinates may be the same, otherwise (except the first and last one) all coordinates need to be distinct
- A linear ring defines two regions on the sphere. ArangoDB will always interpret the region of smaller area to be the interior of the ring. This introduces a practical limitation that no polygon may have an outer ring enclosing more than half the Earth’s surface.
No Holes:
{
"type": "Polygon",
"coordinates": [
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
]
]
}
With Holes:
- The exterior ring should not self-intersect.
- The interior rings must be contained in the outer ring
- No two rings can cross each other, i.e. no ring may intersect both the interior and exterior face of another ring
- Rings cannot share edges, they may however share vertices
- No ring may be empty
- Polygon rings should follow the right-hand rule for orientation (counterclockwise external rings, clockwise internal rings).
{
"type": "Polygon",
"coordinates": [
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
],
[
[100.8, 0.8],
[100.8, 0.2],
[100.2, 0.2],
[100.2, 0.8],
[100.8, 0.8]
]
]
}
MultiPolygon
A GeoJSON MultiPolygon consists of multiple polygons. The “coordinates” member is an array of Polygon coordinate arrays.
- Polygons in the same MultiPolygon may not share edges, they may share coordinates
- Polygons and rings must not be empty
- A linear ring defines two regions on the sphere. ArangoDB will always interpret the region of smaller area to be the interior of the ring. This introduces a practical limitation that no polygon may have an outer ring enclosing more than half the Earth’s surface.
- Linear rings must follow the right-hand rule for orientation (counterclockwise external rings, clockwise internal rings).
Example with two polygons, the second one with a hole:
{
"type": "MultiPolygon",
"coordinates": [
[
[
[102.0, 2.0],
[103.0, 2.0],
[103.0, 3.0],
[102.0, 3.0],
[102.0, 2.0]
]
],
[
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
],
[
[100.2, 0.2],
[100.2, 0.8],
[100.8, 0.8],
[100.8, 0.2],
[100.2, 0.2]
]
]
]
}
Arangosh Examples
ensures that a geo index exists
collection.ensureIndex({ type: "geo", fields: [ "location" ] })
Creates a geospatial index on all documents using location as the path to the coordinates. The value of the attribute has to be an array with at least two numeric values. The array must contain the latitude (first value) and the longitude (second value).
All documents, which do not have the attribute path or have a non-conforming value in it, are excluded from the index.
A geo index is implicitly sparse, and there is no way to control its sparsity.
In case that the index was successfully created, an object with the index details, including the index-identifier, is returned.
To create a geo index on an array attribute that contains longitude first, set
the geoJson attribute to true
. This corresponds to the format described in
RFC 7946 Position
collection.ensureIndex({ type: "geo", fields: [ "location" ], geoJson: true })
To create a geo-spatial index on all documents using latitude and longitude as separate attribute paths, two paths need to be specified in the fields array:
collection.ensureIndex({ type: "geo", fields: [ "latitude", "longitude" ] })
In case that the index was successfully created, an object with the index details, including the index-identifier, is returned.
Examples
Create a geo index for an array attribute:
Create a geo index for a hash array attribute:
Use GeoIndex with AQL SORT statement:
Use GeoIndex with AQL FILTER statement:
constructs a geo index selectioncollection.geo(location-attribute)
Looks up a geo index defined on attribute location_attribute.
Returns a geo index object if an index was found. The near
or
within
operators can then be used to execute a geo-spatial query on
this particular index.
This is useful for collections with multiple defined geo indexes.
collection.geo(location_attribute, true)
Looks up a geo index on a compound attribute location_attribute.
Returns a geo index object if an index was found. The near
or
within
operators can then be used to execute a geo-spatial query on
this particular index.
collection.geo(latitude_attribute, longitude_attribute)
Looks up a geo index defined on the two attributes latitude_attribute and longitude-attribute.
Returns a geo index object if an index was found. The near
or
within
operators can then be used to execute a geo-spatial query on
this particular index.
Note: this method is not yet supported by the RocksDB storage engine.
Note: the geo simple query helper function is deprecated as of ArangoDB 2.6. The function may be removed in future versions of ArangoDB. The preferred way for running geo queries is to use their AQL equivalents.
Examples
Assume you have a location stored as list in the attribute home
and a destination stored in the attribute work. Then you can use the
geo
operator to select which geo-spatial attributes (and thus which
index) to use in a near
query.
arangosh> for (i = -90; i <= 90; i += 10) {
........> for (j = -180; j <= 180; j += 10) {
........> db.complex.save({ name : "Name/" + i + "/" + j,
........> home : [ i, j ],
........> work : [ -i, -j ] });
........> }
........> }
........>
arangosh> db.complex.near(0, 170).limit(5);
[ArangoError 1570: no suitable geo index found for geo restriction on 'complex']
arangosh> db.complex.ensureIndex({ type: "geo", fields: [ "home" ] });
{
"bestIndexedLevel" : 17,
"fields" : [
"home"
],
"geoJson" : false,
"id" : "complex/85116",
"isNewlyCreated" : true,
"maxNumCoverCells" : 8,
"sparse" : true,
"type" : "geo",
"unique" : false,
"worstIndexedLevel" : 4,
"code" : 201
}
arangosh> db.complex.near(0, 170).limit(5).toArray();
[
{
"_key" : "84444",
"_id" : "complex/84444",
"_rev" : "_bHcRV2O---",
"name" : "Name/0/170",
"home" : [
0,
170
],
"work" : [
0,
-170
]
},
{
"_key" : "84446",
"_id" : "complex/84446",
"_rev" : "_bHcRV2O--A",
"name" : "Name/0/180",
"home" : [
0,
180
],
"work" : [
0,
-180
]
},
{
"_key" : "84518",
"_id" : "complex/84518",
"_rev" : "_bHcRV3W---",
"name" : "Name/10/170",
"home" : [
10,
170
],
"work" : [
-10,
-170
]
},
{
"_key" : "84370",
"_id" : "complex/84370",
"_rev" : "_bHcRV1C--A",
"name" : "Name/-10/170",
"home" : [
-10,
170
],
"work" : [
10,
-170
]
},
{
"_key" : "84374",
"_id" : "complex/84374",
"_rev" : "_bHcRV1G--A",
"name" : "Name/0/-180",
"home" : [
0,
-180
],
"work" : [
0,
180
]
}
]
arangosh> db.complex.geo("work").near(0, 170).limit(5);
[ArangoError 1570: no suitable geo index found for geo restriction on 'complex']
arangosh> db.complex.ensureIndex({ type: "geo", fields: [ "work" ] });
{
"bestIndexedLevel" : 17,
"fields" : [
"work"
],
"geoJson" : false,
"id" : "complex/85124",
"isNewlyCreated" : true,
"maxNumCoverCells" : 8,
"sparse" : true,
"type" : "geo",
"unique" : false,
"worstIndexedLevel" : 4,
"code" : 201
}
arangosh> db.complex.geo("work").near(0, 170).limit(5).toArray();
[
{
"_key" : "84444",
"_id" : "complex/84444",
"_rev" : "_bHcRV2O---",
"name" : "Name/0/170",
"home" : [
0,
170
],
"work" : [
0,
-170
]
},
{
"_key" : "84446",
"_id" : "complex/84446",
"_rev" : "_bHcRV2O--A",
"name" : "Name/0/180",
"home" : [
0,
180
],
"work" : [
0,
-180
]
},
{
"_key" : "84518",
"_id" : "complex/84518",
"_rev" : "_bHcRV3W---",
"name" : "Name/10/170",
"home" : [
10,
170
],
"work" : [
-10,
-170
]
},
{
"_key" : "84370",
"_id" : "complex/84370",
"_rev" : "_bHcRV1C--A",
"name" : "Name/-10/170",
"home" : [
-10,
170
],
"work" : [
10,
-170
]
},
{
"_key" : "84374",
"_id" : "complex/84374",
"_rev" : "_bHcRV1G--A",
"name" : "Name/0/-180",
"home" : [
0,
-180
],
"work" : [
0,
180
]
}
]
arangosh> for (i = -90; i <= 90; i += 10) {
........> for (j = -180; j <= 180; j += 10) {
........> db.complex.save({ name : "Name/" + i + "/" + j,
........> home : [ i, j ],
........> work : [ -i, -j ] });
........> }
........> }
........>
arangosh> db.complex.near(0, 170).limit(5);
arangosh> db.complex.ensureIndex({ type: "geo", fields: [ "home" ] });
arangosh> db.complex.near(0, 170).limit(5).toArray();
arangosh> db.complex.geo("work").near(0, 170).limit(5);
arangosh> db.complex.ensureIndex({ type: "geo", fields: [ "work" ] });
arangosh> db.complex.geo("work").near(0, 170).limit(5).toArray();
constructs a near query for a collectioncollection.near(latitude, longitude)
The returned list is sorted according to the distance, with the nearest document to the coordinate (latitude, longitude) coming first. If there are near documents of equal distance, documents are chosen randomly from this set until the limit is reached. It is possible to change the limit using the limit operator.
In order to use the near operator, a geo index must be defined for the collection. This index also defines which attribute holds the coordinates for the document. If you have more then one geo-spatial index, you can use the geo operator to select a particular index.
Note: near
does not support negative skips.
// However, you can still use limit
followed to skip.
collection.near(latitude, longitude).limit(limit)
Limits the result to limit documents instead of the default 100.
Note: Unlike with multiple explicit limits, limit
will raise
the implicit default limit imposed by within
.
collection.near(latitude, longitude).distance()
This will add an attribute distance
to all documents returned, which
contains the distance between the given point and the document in meters.
collection.near(latitude, longitude).distance(name)
This will add an attribute name to all documents returned, which contains the distance between the given point and the document in meters.
Note: this method is not yet supported by the RocksDB storage engine.
Note: the near simple query function is deprecated as of ArangoDB 2.6. The function may be removed in future versions of ArangoDB. The preferred way for retrieving documents from a collection using the near operator is to use the AQL NEAR function in an AQL query as follows:
FOR doc IN NEAR(@@collection, @latitude, @longitude, @limit)
RETURN doc
Examples
To get the nearest two locations:
arangosh> db.geo.ensureIndex({ type: "geo", fields: [ "loc" ] });
{
"bestIndexedLevel" : 17,
"fields" : [
"loc"
],
"geoJson" : false,
"id" : "geo/223",
"isNewlyCreated" : true,
"maxNumCoverCells" : 8,
"sparse" : true,
"type" : "geo",
"unique" : false,
"worstIndexedLevel" : 4,
"code" : 201
}
arangosh> for (var i = -90; i <= 90; i += 10) {
........> for (var j = -180; j <= 180; j += 10) {
........> db.geo.save({
........> name : "Name/" + i + "/" + j,
........> loc: [ i, j ] });
........> } }
arangosh> db.geo.near(0, 0).limit(2).toArray();
[
{
"_key" : "927",
"_id" : "geo/927",
"_rev" : "_bHcQxRK--A",
"name" : "Name/0/0",
"loc" : [
0,
0
]
},
{
"_key" : "1001",
"_id" : "geo/1001",
"_rev" : "_bHcQxSW--A",
"name" : "Name/10/0",
"loc" : [
10,
0
]
}
]
arangosh> db.geo.ensureIndex({ type: "geo", fields: [ "loc" ] });
arangosh> for (var i = -90; i <= 90; i += 10) {
........> for (var j = -180; j <= 180; j += 10) {
........> db.geo.save({
........> name : "Name/" + i + "/" + j,
........> loc: [ i, j ] });
........> } }
arangosh> db.geo.near(0, 0).limit(2).toArray();
If you need the distance as well, then you can use the distance
operator:
arangosh> db.geo.ensureIndex({ type: "geo", fields: [ "loc" ] });
{
"bestIndexedLevel" : 17,
"fields" : [
"loc"
],
"geoJson" : false,
"id" : "geo/1643",
"isNewlyCreated" : true,
"maxNumCoverCells" : 8,
"sparse" : true,
"type" : "geo",
"unique" : false,
"worstIndexedLevel" : 4,
"code" : 201
}
arangosh> for (var i = -90; i <= 90; i += 10) {
........> for (var j = -180; j <= 180; j += 10) {
........> db.geo.save({
........> name : "Name/" + i + "/" + j,
........> loc: [ i, j ] });
........> } }
arangosh> db.geo.near(0, 0).distance().limit(2).toArray();
[
{
"_id" : "geo/2347",
"_key" : "2347",
"_rev" : "_bHcQxnW--A",
"loc" : [
0,
0
],
"name" : "Name/0/0",
"distance" : 0
},
{
"_id" : "geo/2421",
"_key" : "2421",
"_rev" : "_bHcQxoe---",
"loc" : [
10,
0
],
"name" : "Name/10/0",
"distance" : 1111949.2664455872
}
]
arangosh> db.geo.ensureIndex({ type: "geo", fields: [ "loc" ] });
arangosh> for (var i = -90; i <= 90; i += 10) {
........> for (var j = -180; j <= 180; j += 10) {
........> db.geo.save({
........> name : "Name/" + i + "/" + j,
........> loc: [ i, j ] });
........> } }
arangosh> db.geo.near(0, 0).distance().limit(2).toArray();
constructs a within query for a collectioncollection.within(latitude, longitude, radius)
This will find all documents within a given radius around the coordinate (latitude, longitude). The returned array is sorted by distance, beginning with the nearest document.
In order to use the within operator, a geo index must be defined for the
collection. This index also defines which attribute holds the coordinates
for the document. If you have more then one geo-spatial index, you can use
the geo
operator to select a particular index.
collection.within(latitude, longitude, radius).distance()
This will add an attribute _distance
to all documents returned, which
contains the distance between the given point and the document in meters.
collection.within(latitude, longitude, radius).distance(name)
This will add an attribute name to all documents returned, which contains the distance between the given point and the document in meters.
Note: this method is not yet supported by the RocksDB storage engine.
Note: the within simple query function is deprecated as of ArangoDB 2.6. The function may be removed in future versions of ArangoDB. The preferred way for retrieving documents from a collection using the within operator is to use the AQL WITHIN function in an AQL query as follows:
FOR doc IN WITHIN(@@collection, @latitude, @longitude, @radius, @distanceAttributeName)
RETURN doc
Examples
To find all documents within a radius of 2000 km use:
arangosh> for (var i = -90; i <= 90; i += 10) {
........> for (var j = -180; j <= 180; j += 10) {
........> db.geo.save({ name : "Name/" + i + "/" + j, loc: [ i, j ] }); } }
arangosh> db.geo.within(0, 0, 2000 * 1000).distance().toArray();
[
{
"_id" : "geo/3767",
"_key" : "3767",
"_rev" : "_bHcQx8O--A",
"loc" : [
0,
0
],
"name" : "Name/0/0",
"distance" : 0
},
{
"_id" : "geo/3841",
"_key" : "3841",
"_rev" : "_bHcQx9---C",
"loc" : [
10,
0
],
"name" : "Name/10/0",
"distance" : 1111949.2664455872
},
{
"_id" : "geo/3769",
"_key" : "3769",
"_rev" : "_bHcQx8O--C",
"loc" : [
0,
10
],
"name" : "Name/0/10",
"distance" : 1111949.2664455872
},
{
"_id" : "geo/3693",
"_key" : "3693",
"_rev" : "_bHcQx7a--C",
"loc" : [
-10,
0
],
"name" : "Name/-10/0",
"distance" : 1111949.2664455872
},
{
"_id" : "geo/3765",
"_key" : "3765",
"_rev" : "_bHcQx8O---",
"loc" : [
0,
-10
],
"name" : "Name/0/-10",
"distance" : 1111949.2664455872
},
{
"_id" : "geo/3691",
"_key" : "3691",
"_rev" : "_bHcQx7a--A",
"loc" : [
-10,
-10
],
"name" : "Name/-10/-10",
"distance" : 1568520.556798576
},
{
"_id" : "geo/3843",
"_key" : "3843",
"_rev" : "_bHcQx9C---",
"loc" : [
10,
10
],
"name" : "Name/10/10",
"distance" : 1568520.556798576
},
{
"_id" : "geo/3839",
"_key" : "3839",
"_rev" : "_bHcQx9---A",
"loc" : [
10,
-10
],
"name" : "Name/10/-10",
"distance" : 1568520.556798576
},
{
"_id" : "geo/3695",
"_key" : "3695",
"_rev" : "_bHcQx7e---",
"loc" : [
-10,
10
],
"name" : "Name/-10/10",
"distance" : 1568520.556798576
}
]
arangosh> for (var i = -90; i <= 90; i += 10) {
........> for (var j = -180; j <= 180; j += 10) {
........> db.geo.save({ name : "Name/" + i + "/" + j, loc: [ i, j ] }); } }
arangosh> db.geo.within(0, 0, 2000 * 1000).distance().toArray();
ensures that a geo index exists
collection.ensureIndex({ type: "geo", fields: [ "location" ] })
Since ArangoDB 2.5, this method is an alias for ensureGeoIndex since geo indexes are always sparse, meaning that documents that do not contain the index attributes or has non-numeric values in the index attributes will not be indexed. ensureGeoConstraint is deprecated and ensureGeoIndex should be used instead.
The index does not provide a unique
option because of its limited usability.
It would prevent identical coordinates from being inserted only, but even a
slightly different location (like 1 inch or 1 cm off) would be unique again and
not considered a duplicate, although it probably should. The desired threshold
for detecting duplicates may vary for every project (including how to calculate
the distance even) and needs to be implemented on the application layer as
needed. You can write a Foxx service for this purpose and
make use of the AQL geo functions to find nearby
coordinates supported by a geo index.