Integrating Naurt’s Geocoder with Swift

A detailed guide to Naurt geocoding in swift

Indigo Curnick
September 16, 2024
Resources

NOTE: As of right now, this will only work on MacOS and Linux machines. The server dependency, Vapor, is currently not supported on Microsoft Windows. Please see https://github.com/vapor/vapor/issues/2646 and https://github.com/apple/swift-nio/issues/2065. We’ll update this blog once Windows support comes, hopefully soon!

Setting Up the Environment

The set up will vary slightly depending on what platform you use. I will be setting this up on an ArchLinux machine for use with the command line. If you’re using a different platform, see the instructions here.

On ArchLinux, you can use yay to install Swift

yay -S swift-bin

Then initialise the Swift project with

swift package init --type executable

We’ll use Vapor as the web, server, so install it by making this the contents of your Package.swift file

1// swift-tools-version: 5.10
2// The swift-tools-version declares the minimum version of Swift required to build this package.
3
4import PackageDescription
5
6let package = Package(
7    name: "NaurtExample",
8    dependencies: [
9        .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0")
10    ],
11    targets: [
12        // Targets are the basic building blocks of a package, defining a module or a test suite.
13        // Targets can depend on other targets in this package and products from dependencies.
14        .executableTarget(
15            name: "NaurtExample",
16            dependencies: [
17                .product(name: "Vapor", package: "vapor")
18            ]
19            ),
20    ]
21)

And then fetch the dependencies with

swift package update

Setting Up a Basic Web Server

We’ll create a basic web server with Vapor. I deviate from the Vapor introductory docs slightly in the implementation - I don’t think the example there is as illuminating. I think the most scalable way to structure this is with three elements

  1. A main  function where top-level configuration can take place
  2. A function which let’s us associate endpoints with specific handler functions
  3. Handler functions themselves

So, the implementation we’ll use is the following

1import Vapor
2
3func main() throws {
4    var env = try Environment.detect()
5    let app = Application(env)
6    defer { app.shutdown() }
7
8    try configureRoutes(app)
9
10    try app.run()
11}
12
13func configureRoutes(_ app: Application) throws {
14    app.get(use: helloWorldHandler)
15}
16
17func helloWorldHandler(req: Request) -> Response {
18    return Response(status: .ok, body: .init(string: "Thanks for calling me!"))
19}
20
21try main()

At this point you can run this with

swift build
swift run

And then go to http://localhost:8080 - if everything went well you should see a simple reply from the server.

A Free Naurt API 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. Make sure the file is next to the Package.swift file. In most backend applications, this will probably be injected via an environment variable or a secret.

For us, just having this as a global variable will suffice, so we can place this at the top of the main.swift file

1struct Config {
2    static let apiKey: String = {
3        let filePath = "api.key"
4        do {
5            let fileContents = try String(contentsOfFile: filePath, encoding: .utf8)
6            return fileContents
7        } catch {
8            fatalError("API Key file not found")
9        }
10    }()
11}

Just before the main function executes the API key will be stored in this struct.

Basic Naurt Request

Obviously our web server will be far more useful once we actually make requests to Naurt. Let’s replace our handler function with the following code

1func handler(req: Request) async throws -> Response {
2    let response = try await req.client.post("https://api.naurt.net/final-destination/v1") { req in
3
4    try req.content.encode(["address_string": "The Grand Hotel, Brighton"])
5
6    req.headers.add(name: "Authorization", value: Config.apiKey)
7}
8
9    guard response.status == .ok else {
10        return Response(status: .internalServerError, body: .init(string: "Failed to fetch data from the external API"))
11    }
12
13    var headers = HTTPHeaders()
14    headers.add(name: .contentType, value: "application/json")
15    let jsonString = String(buffer: response.body!)
16
17    return Response(status: .ok, headers: headers, body: .init(string: jsonString))
18
19}

Just a few things to note here - we’ve hard coded in a request, but we’ll come back to that later. We use let jsonString = String(buffer: response.body!) to convert the bytes of the response into a utf8 string which we can actually send back to the user.

Handling Query Parameters

Hard coding in a request isn’t so useful, since we’d have to recompile the program every time we wanted a new request. Instead, we’ll use query parameters in the URL. Before we do that, we need to create a helper struct to handle the encoding into a JSON. However, there’s two things we need to take note of

  1. In Swift, variable names are supposed to be camel case, but the Naurt JSON is snake case. So, we’ll store them as camel case in the code and only convert to snake case when we encode to a JSON
  2. Some of the parameters are optional, and we shouldn’t encode them when they aren’t present

Therefore, make the following struct. We make it conform to Content so it can fit into the Vapor ecosystem.

1struct NaurtRequest: Content {
2    let addressString: String?
3    let additionalMatches: Bool?
4    let latitude: Float?
5    let longitude: Float?
6
7    func encode(to encoder: any Encoder) throws {
8        var container = encoder.container(keyedBy: CustomCodingKeys.self)
9
10        if let addressString = self.addressString {
11            try container.encode(addressString, forKey: .addressString)
12        }
13
14        if let additionalMatches = self.additionalMatches {
15            try container.encode(additionalMatches, forKey: .additionalMatches)
16        }
17
18        if let latitude = self.latitude {
19            try container.encode(latitude, forKey: .latitude)
20        }
21
22        if let longitude = self.longitude {
23            try container.encode(longitude, forKey: .longitude)
24        }
25    }
26
27    enum CustomCodingKeys: String, CodingKey {
28        case addressString = "address_string"
29        case additionalMatches = "additional_matches"
30        case latitude = "latitude"
31        case longitude = "longitude"
32    }
33}

Once we have that, actually parsing the query parameters is really easy in Vapor, we just need to update the handler function like so

1func handler(req: Request) async throws -> Response {
2
3    let addressString = req.query[String.self, at: "address_string"]
4    let additionalMatches = req.query[Bool.self, at: "additional_matches"]
5    let latitude = req.query[Float.self, at: "latitude"]
6    let longitude = req.query[Float.self, at: "longitude"]
7
8    let response = try await req.client.post("https://api.naurt.net/final-destination/v1") { req in
9
10    let naurtRequest = NaurtRequest(addressString: addressString, additionalMatches: additionalMatches, latitude: latitude, longitude: longitude)
11    try req.content.encode(naurtRequest)
12   
13    req.headers.add(name: "Authorization", value: Config.apiKey)
14}
15// Rest of the function...

Now you can try going to http://localhost:8080/?address_string=The Grand Hotel, Brighton to see this working!

Visualising the GeoJSON

The majority of the data returned by Naurt is inside a GeoJSON. While this is a great format, it isn’t really designed for humans to read. The best way to visualise the GeoJSON is of course on a map. We’ll use a simple Leaflet script to do that, and we’ll insert the Naurt response using Lead - Vapor’s templating library. To install Leaf, update the Package.swift like so

1// swift-tools-version: 5.10
2// The swift-tools-version declares the minimum version of Swift required to build this package.
3
4import PackageDescription
5
6let package = Package(
7    name: "NaurtExample",
8    dependencies: [
9        .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
10        .package(url: "https://github.com/vapor/leaf.git", from: "4.0.0")
11    ],
12    targets: [
13        // Targets are the basic building blocks of a package, defining a module or a test suite.
14        // Targets can depend on other targets in this package and products from dependencies.
15        .executableTarget(
16            name: "NaurtExample",
17            dependencies: [
18                .product(name: "Vapor", package: "vapor"),
19                .product(name: "Leaf", package: "leaf")
20            ]
21            ),
22    ]
23)
24

Don’t forget to update the packages with

swift package update

Create a folder next to Sources called Resources and then a Views folder inside that. Create a file called index.leaf . This will be our template, and the contents are

1<!DOCTYPE html>
2<html>
3
4<head>
5    <title>Naurt Geocoder</title>
6    <meta charset="utf-8" />
7    <meta name="viewport" content="width=device-width, initial-scale=1.0">
8    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
9    <style>
10        #map {
11            height: 100vh;
12        }
13    </style>
14</head>
15
16<body>
17    <div id="map"></div>
18    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
19    <script>
20
21        function getColor(type) {
22            switch (type) {
23                case 'naurt_door': return '#FF0000';
24                case 'naurt_building': return '#9d79f2';
25                case 'naurt_parking': return '#00FF00';
26                default: return '#000000';
27            }
28        }
29
30        function style(feature) {
31            return {
32                fillColor: getColor(feature.properties.naurt_type), weight: 2, opacity: 1, color: 'white', dashArray: '5', fillOpacity: 0.4
33            };
34        }
35
36        function onEachFeatureCurry(address) {
37            return function onEachFeature(feature, layer) {
38                if (feature.properties) {
39                    var popupContent = 'Type: ' + feature.properties.naurt_type;
40                    if (feature.properties.naurt_type == 'naurt_building') {
41                        popupContent += '<br>Address: ' + address;
42                    }
43                    layer.bindPopup(popupContent).openPopup();
44                }
45            }
46        }
47
48
49
50        var map = L.map('map').setView([0, 0], 2);
51        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(map);
52        var response = #unsafeHTML(NaurtResponse);
53
54
55        L.geoJSON(response["best_match"]["geojson"], { style: style, onEachFeature: onEachFeatureCurry(response["best_match"]["address"]) }).addTo(map);
56
57        if ("additional_matches" in response) {
58            for (const additionalMatch of response["additional_matches"]) {
59                L.geoJSON(additionalMatch["geojson"], { style: style, onEachFeature: onEachFeatureCurry(additionalMatch["address"]) }).addTo(map);
60            }
61        }
62
63        var bounds = L.geoJSON(response["best_match"]["geojson"]).getBounds();
64        map.fitBounds(bounds);
65    </script>
66</body>
67
68
69</html>

I won’t walk through every detail of the template, as it’s pretty self-explanatory. I’ll just note that the basic idea here is to plot each aspect of the GeoJSON - naurt_door, naurt_parking, naurt_building - as its own layer. We also check to see if there are additional_matches, and if there are, loop over them and plot them in the same way as the best_match.

To actually use the template we just need to do a few things.

First, import Leaf at the top of main.swift

import Leaf

Then add Leaf to the main function

1func main() throws {
2
3    var env = try Environment.detect()
4    let app = Application(env)
5    defer { app.shutdown() }
6
7    app.views.use(.leaf)
8
9    try configureRoutes(app)
10
11    try app.run()
12}

And then just update the end of handler

1 	  // Rest of function...		 
2    guard response.status == .ok else {
3        return Response(status: .internalServerError, body: .init(string: "Failed to fetch data from the external API"))
4    }
5
6    let jsonString = String(buffer: response.body!)
7
8    let context = ["NaurtResponse": jsonString]
9    return try await req.view.render("index", context).encodeResponse(status: .ok, for: req)
10}

This will render the template, and also encode it to a Response. Normally, with Leaf templates you return a EventLoopFuture<View>  however, we sometimes need to return different kinds of responses (when there’s an error), so that doesn’t work here.

And that’s it!

If you run the server again and go to http://localhost:8080/?address_string=The Grand Hotel, Brighton&additional_matches=true you should see the following

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.