Naurt’s geocoder is a simple solution to many of the challenges faced by last mile delivery companies. Naurt provides you with more than the building location - it also gives the parking spots and building entrances. Delivery drivers can find exactly where to hand off goods. This leads to faster deliveries and better customer satisfaction!
In this blog, we’ll look at how to get the best from Naurt’s geocoder, in Python. For even more information on the API, see the docs.
If you want to follow along with the complete code you can find it here.
Sign Up for your FREE Naurt Key
If you haven’t already, sign up at Naurt’s dashboard for a free key. The free key comes loaded with thousands of API requests per month, so there’s no cost to try it out. We don’t require a credit card for sign up, either!
For this post, I’ll be placing my API key in a file called api.key
. In most backend applications, this will probably be injected via an environment variable or a secret.
Setting up the Environment in Python
I’ll use a virtual environment, which you can create and activate with
python -m venv .venv
source .venv/bin/activate
If you’re on Windows then usually you can activate using .venv/Scripts/activate.bat
in cmd and .venv/Scripts/Activate.ps1
in PowerShell. Here’s a guide that talks more about virtual environment management.
I’ll also make a requirements.txt
file, and for now we’ll add just one entry - requests
. The requests
library helps us make HTTP requests. You can then install requests
into the virtual environment with (make sure the virtual environment is active or this will install into your global Python install - which is NOT recommended)
pip install -r requirements.txt
To template this code, I’ll start with the following in a main.py
file next to api.key
import requests
ENDPOINT = "https://api.naurt.net/final-destination/v1"
with open("api.key") as file:
API_KEY = file.read()
def main():
pass
if __name__ == "__main__":
main()
Now we have some basic structure, the endpoint, as well as our Naurt API key stored in API_KEY,
we’re ready to begin.
How to Forward Geocode
Let’s start with a simple request. Naurt’s geocoder couldn’t be easier to use - all we need is a simple JSON body like this
{
"address_string": String
}
And some headers - Authorization
for our API key and content-type: application/json
Let’s program that! I’ll place the following in my main()
function
headers = {"Authorization": API_KEY, "content-type": "application/json"}
body = {"address_string": "Grand Hotel, Brighton"}
response = requests.post(ENDPOINT, headers=headers, json=body)
print(response.json())
If we run this, we get the output we’re looking for!
{
"best_match": {
"address": "Grand Hotel, 97-99, Kings Road, Brighton, East Sussex, England, United Kingdom, BN1 2FW",
"geojson": {},
"id": "a739c2bc-a45c-374c-d477-8f8dd8081c1c"
},
"contributors": [
"© OpenStreetMap contributors"
],
"version": "1.1.0"
}
(I’ve just collapsed the geojson
field for readability)
How to do a Structured API Request in Python
When making requests to APIs, it’s helpful to have a class which you can serialise to avoid mistakes. For this purpose, I’ll be using the pydantic
library - remember to add it to your
requirements.txt
and run the installation again with pip install -r requirements.txt
. We need to import BaseModel
from pydantic
as well as Optional
from typing
from pydantic import BaseModel
from typing import Optional
Then write the NaurtRequest
class
class NaurtRequest(BaseModel):
address_string: Optional[str]
Now, we can make the body in the following way
body = NaurtRequest(address_string="Grand Hotel, Brighton").dict()
And that’s it - we now have a structured request. We’ll come back later and add some more fields to this. If you run the file again you should get the same output.
How to do a Structured API Response in Python
Like the structured request, we can structure the response too. Let’s make our classes like so
class DestinationRresponse(BaseModel):
id: str
address: str
geojson: Dict
distance: Optional[float] = None
class NaurtResponse(BaseModel):
best_match: Optional[DestinationRresponse] = None
additional_matches: Optional[List[DestinationRresponse]] = None
version: Optional[str] = None
Make sure to also import Dict
and List
from typing
!
Here, we set the defaults to None
- this just means if those fields aren’t present then pydantic
will handle them gracefully for us. So, instead of using the json()
method on the reply, we can actually do this instead.
response = requests.post(ENDPOINT, headers=headers, json=body)
naurt_response = NaurtResponse.parse_raw(response.text)
print(naurt_response)
naurt_response
will now be of type NaurtResponse
, which is much more useful, and we can see an example of that in the next section.
Advanced Geocoder Search Options
Let’s explore some of the alternative search options we can try. We can update the NaurtRequest
to look like this
class NaurtRequest(BaseModel):
address_string: Optional[str] = None
country: Optional[str] = None
latitude: Optional[float] = None
longitude: Optional[float] = None
distance_filter: Optional[float] = None
additional_matches: Optional[bool] = None
Since we’ll have a bunch of fields which will be None
most of the time, we should use the exclude_none=True
option of the dict()
method from pydantic
body = NaurtRequest(
address_string="5 Foundry St, Brighton and Hove, Brighton BN1 4AT"
).dict(exclude_none=True)
This way, the dictionary we get only has an address_string
field.
So, what do these additional fields do?
With just a latitude
and longitude
we can do a reverse geocode. This finds nearby addresses. This works best if we set additional_matches
to true
so we get a total of 5 addresses back
body = NaurtRequest(latitude=50.83, longitude=-0.13, additional_matches=True).dict(
exclude_none=True
)
Any extra matches beyond the best match will go into the additional_matches
field of the NaurtResponse
.
We can also search by address and location at the same time - this finds the best match for the address string provided limited to the distance from the point specific by distance_filter
. If no distance_filter
is provided, the default is 5000 metres. You can set a maximum distance of 25000 metres. This mode is helpful for finding more generic things in an area - for example, you can do this to find churches in and around Brighton, England
body = NaurtRequest(
address_string="Church",
latitude=50.83,
longitude=-0.13,
additional_matches=True,
distance_filter=25000,
).dict(exclude_none=True)
How to Plot Geocoder Responses on a Map
Up until now, we haven’t really done too much with the response other than print it out. The most intuitive way to understand geographical information is to plot it on a map. So let’s do that!
The general gist here will be to use Kepler to plot the geojson and use Flask to serve it - so we need to add
keplergl
setuptools
flask
to the requirements.txt
and then run pip install -r requirements.txt
again to install those.
We need to convert the main function now into a flask function
from flask import Flask
from keplergl import KeplerGl
app = Flask(__name__)
@app.get("/")
def main():
headers = {"Authorization": API_KEY, "content-type": "application/json"}
body = NaurtRequest(
address_string="Grand Hotel",
latitude=50.83,
longitude=-0.13,
additional_matches=True,
distance_filter=25000,
).dict(exclude_none=True)
response = requests.post(ENDPOINT, headers=headers, json=body)
naurt_response = NaurtResponse.parse_raw(response.text)
my_map = KeplerGl()
for feature in naurt_response.best_match.geojson["features"]:
my_map.add_data(data=feature, name=feature["properties"]["naurt_type"])
html = my_map._repr_html_()
return html, 200
We also don’t want to call the main
function directly anymore, so we want to do this instead
if __name__ == "__main__":
app.run(debug=True)
So, if we run this now, we can go to localhost:5000
in our browser and see the plot
By default, the map will centre over San Francisco in the United States, but if you scroll over to Brighton, England you’ll see this (or you can change it to any address you like). We’ll fix that in the next section and make the map jump to the right place to start.
We can see the building outline (if available), the door and the parking zone!
Going Further
There’s three things we can do now to improve the usability of our little demo app
- Centre the map
- Also plot additional matches
- Add search parameters to the URL
To add search parameters, we need to import request
from Flask and then add this into the main function
address = request.args.get("address", type=str)
latitude = request.args.get("latitude", type=float)
longitude = request.args.get("longitude", type=float)
distance = request.args.get("distance", type=int)
body = NaurtRequest(
address_string=address,
latitude=latitude,
longitude=longitude,
additional_matches=True,
distance_filter=distance,
).dict(exclude_none=True)
We can go to this URL http://localhost:5000/?address=Grand Hotel, Brighton?latitude=50.12?longitude?=-0.13?distance=25000
to see the map load. We don’t have to restart the app to get a new location now!
Let’s sort out the map centre and zoom. First, we’ll make this function
def make_config(latitude: float, longitude: float) -> Dict:
config = {
"version": "v1",
"config": {
"mapState": {
"latitude": latitude,
"longitude": longitude,
"zoom": 15
}
}
}
return config
Then we’ll replace the map code with this
for feature in naurt_response.best_match.geojson["features"]:
my_map.add_data(data=feature, name="{} - {}".format(naurt_response.best_match.address, feature["properties"]["naurt_type"]))
if feature["properties"]["naurt_type"] == "naurt_door":
config = make_config(feature["geometry"]["coordinates"][0][1], feature["geometry"]["coordinates"][0][0])
my_map.config = config
for additional_match in naurt_response.additional_matches:
for feature in additional_match.geojson["features"]:
my_map.add_data(data=feature, name="{} - {}".format(additional_match.address, feature["properties"]["naurt_type"]))
This will plot all of the matches, as well as centre the map on the door of the best match!
You can see the complete code on our GitHub.