This GEOS example program demonstrates the use of the STRtree index and PreparedGeometry to create a high-performance in-memory reverse geocoder.
A generic reverse geocoder takes in coordinates and returns addresses. This simple application works with collections of polygons. It takes in a query coordinate and returns data from the polygons that the query intersects with.
In order to efficiently search a large collection of polygons, the server applies two indexes to the collection:
- Each polygon is internally indexed by creating a PreparedGeometry from the geometry.
- The geometries are then all added to a TemplateSTRtree.
When a coordinate comes in, the query is executed by:
- First querying the STRtree, to find all geometries that have bounding boxes that intersect the coordinate.
- Second for each geometry found, running the prepared geometry intersects() method to determine if the point is actually inside the geometry.
Basically the two indexes act at different levels. The STRtree indexes the bounds of all the polygons. The PreparedGeometry indexes the segments of each separate geometry.
The lookup service is contained within the SpatialLookup
class. This class holds the parsed GeoJSON objects, which in turn hold the properties list for each object. It also holds the STRtree index, and a vector of LookupEntry
objects for each polygonal GeoJSONFeature
.
Within the SpatialLookup
class the LookupEntry
class encapsulates the PreparedGeometry
and GeoJSONFeature
into a single object that can be added to the STRtree. This reduces the lookup code to the bare minimum:
- find all entries in the STRtree that intersect the query,
- check each entry for intersection,
- read the desired property from the GeoJSON properties of each intersecting entry.
The HTTP interface is provided here by cpp-httplib, but it could be provided by any HTTP library you choose.
To build the code, create a clean build directory and then build. Using a separate build directory makes cleaning up build artifacts very easy.
mkdir _build
cd _build
cmake ..
make
To run the application, ensure you have a GeoJSON FeatureCollection file of polygons handy. (Use the example below, or download a state zip code file perhaps.)
Then run the executable, supplying the GeoJSON file name and the name of the property you want returned when your query point hits a polygon. For example, using a Maryland zipcode file:
# ./spatial_lookup md_maryland_zip_codes_geo.min.json ZCTA5CE10
./spatial_lookup loaded and indexed md_maryland_zip_codes_geo.min.json
./spatial_lookup listening on localhost:8080
Querying the service then looks like this:
curl "http://localhost:8080/lookup?x=-78.40&y=39.69"
["21766"]
Use "name" as your property.
./spatial_lookup geojson.json name
There two polygons surrounding the cell at 0,0.
curl http://localhost:8080/lookup?x=0.5&y=0.5
Example data for geojson.json
file.
{ "type": "FeatureCollection",
"features": [
{ "type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0],
[0.0, 1.0], [0.0, 0.0] ]
]
},
"properties": {
"name": "Peter",
"age": 50.8
}
},
{ "type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[ [0.0, 0.0], [1.0, 0.0], [1.0, 1.0],
[0.0, 1.0], [0.0, 0.0] ]
]
},
"properties": {
"name": "Paul",
"age": 50.8
}
},
{ "type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[ [10.0, 10.0], [11.0, 10.0], [11.0, 11.0],
[10.0, 11.0], [10.0, 10.0] ]
]
},
"properties": {
"name": "Mary",
"age": 50.8
}
}
]
}