Integrating Naurt’s Geocoder with Python

In this blog post, you will learn about integrating Naurt’s geocoder with Python to enhance last mile delivery. The guide includes steps to sign up for a free Naurt key, set up the environment, make geocoding requests, structure API responses, and plot geocoder responses on a map using Flask and Kepler. You’ll also explore advanced geocoding options and how to improve the usability of a demo app.

Indigo Curnick
June 20, 2024
Resources

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

1import requests
2
3ENDPOINT = "https://api.naurt.net/final-destination/v1"
4
5with open("api.key") as file:
6    API_KEY = file.read()
7    
8def main():
9    pass
10
11if __name__ == "__main__":
12    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

1headers = {"Authorization": API_KEY, "content-type": "application/json"}
2
3body = {"address_string": "Grand Hotel, Brighton"}
4
5response = requests.post(ENDPOINT, headers=headers, json=body)
6
7print(response.json())

If we run this, we get the output we’re looking for!

1{
2    "best_match": {
3        "address": "Grand Hotel, 97-99, Kings Road, Brighton, East Sussex, England, United Kingdom, BN1 2FW", 
4        "geojson": {}, 
5        "id": "a739c2bc-a45c-374c-d477-8f8dd8081c1c"
6    }, 
7    "contributors": [
8        "© OpenStreetMap contributors"
9    ], 
10    "version": "1.1.0"
11}

(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

1class DestinationRresponse(BaseModel):
2    id: str
3    address: str
4    geojson: Dict
5    distance: Optional[float] = None
6
7
8class NaurtResponse(BaseModel):
9    best_match: Optional[DestinationRresponse] = None
10    additional_matches: Optional[List[DestinationRresponse]] = None
11    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.

1response = requests.post(ENDPOINT, headers=headers, json=body)
2
3naurt_response = NaurtResponse.parse_raw(response.text)
4 
5print(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

1class NaurtRequest(BaseModel):
2    address_string: Optional[str] = None
3    country: Optional[str] = None
4    latitude: Optional[float] = None
5    longitude: Optional[float] = None
6    distance_filter: Optional[float] = None
7    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

1body = NaurtRequest(
2        address_string="5 Foundry St, Brighton and Hove, Brighton BN1 4AT"
3    ).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

1body = NaurtRequest(latitude=50.83, longitude=-0.13, additional_matches=True).dict(
2        exclude_none=True
3    )

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

1from flask import Flask
2from keplergl import KeplerGl
3
4app = Flask(__name__)
5
6@app.get("/")
7def main():
8    headers = {"Authorization": API_KEY, "content-type": "application/json"}
9
10    body = NaurtRequest(
11        address_string="Grand Hotel",
12        latitude=50.83,
13        longitude=-0.13,
14        additional_matches=True,
15        distance_filter=25000,
16    ).dict(exclude_none=True)
17    response = requests.post(ENDPOINT, headers=headers, json=body)
18
19    naurt_response = NaurtResponse.parse_raw(response.text)
20
21    my_map = KeplerGl()
22
23    for feature in naurt_response.best_match.geojson["features"]:
24        my_map.add_data(data=feature, name=feature["properties"]["naurt_type"])
25    
26    html = my_map._repr_html_()
27
28    return html, 200

We also don’t want to call the main function directly anymore, so we want to do this instead

1if __name__ == "__main__":
2   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

  1. Centre the map
  2. Also plot additional matches
  3. 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

1address = request.args.get("address", type=str)
2latitude = request.args.get("latitude", type=float)
3longitude = request.args.get("longitude", type=float)
4distance = request.args.get("distance", type=int)
5
6body = NaurtRequest(
7    address_string=address,
8    latitude=latitude,
9    longitude=longitude,
10    additional_matches=True,
11    distance_filter=distance,
12).dict(exclude_none=True)

We can go to this URL http://localhost:5000/?address=Grand%20Hotel,%20Brighton&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

1def make_config(latitude: float, longitude: float) -> Dict:
2    config = {
3                "version": "v1",
4                "config": {
5                    "mapState": {
6                        "latitude": latitude,
7                        "longitude": longitude,
8                        "zoom": 15
9                    }
10                }
11            }
12    
13    return config

Then we’ll replace the map code with this

1for feature in naurt_response.best_match.geojson["features"]:
2    my_map.add_data(data=feature, name="{} - {}".format(naurt_response.best_match.address, feature["properties"]["naurt_type"]))
3
4    if feature["properties"]["naurt_type"] == "naurt_door":
5        config = make_config(feature["geometry"]["coordinates"][0][1], feature["geometry"]["coordinates"][0][0])
6        my_map.config = config
7        
8
9for additional_match in naurt_response.additional_matches:
10    for feature in additional_match.geojson["features"]:
11        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.

Subscribe To Our Newsletter - Sleek X Webflow Template

Subscribe to our newsletter

Sign up at Naurt for product updates, and stay in the loop!

Thanks for subscribing to our newsletter
Oops! Something went wrong while submitting the form.