Integrarting Naurt’s Geocoder with Go

Indigo Curnick
July 8, 2024
Resources

Setting up the Environment with Go

If you haven’t yet, make sure to install Go. You can follow the official instructions here, but on ArchLinux you can install with

$ sudo pacman -S go

And then add Go to your PATH with

export PATH=$PATH:/usr/local/go/bin

In either your .zshrc or .bashrc, whichever you use. Confirm your Go installation with

$ go version

To set up the project itself, let’s create a folder, initialise a module, and create our source files.

$ mkdir naurt_example
$ go mod init naurt_example
$ mkdir src
$ touch src/main.go

A Simple Webserver

We can place the following boilerplate in main.go

1package main
2
3import (
4	"fmt"
5	"net/http"
6)
7
8func main() {
9
10	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
11		fmt.Fprintf(w, "Hello world!")
12	})
13
14	fmt.Println("Starting the server!")
15	if err := http.ListenAndServe(":8080", nil); err != nil {
16		fmt.Println((err))
17	}
18}
19

And then run with

$ go build -C src -o naurt
$ ./src/naurt

If you use Windows, you will need to use the following

$ go build -C src -o naurt.exe
$ src/naurt.exe

You should see the web server starting, and if you go to localhost:8080/ in a web browser see a hello world message

Using a Naurt API Key

Using Naurt services requires an API Key. If you don’t already have one, you can sign up to the dashboard for free. We don’t require a credit card. You’ll get a free key loaded with thousands of requests. I’ll place my api key in a file called api.key next to the go.mod file.

In Go, we’ll read the key in as a constant from a file. You’ll need to import os , io and sync. Start by creating the apiKey object

1var (
2	apiKey string
3	once   sync.Once
4)
5
6func initialiseApiKey() {
7	// Use sync.Once to ensure this is executed only once
8	once.Do(func() {
9		// Read file content from a file
10		file, err := os.Open("api.key")
11		if err != nil {
12			panic("`api.key` not found")
13		}
14
15		defer file.Close()
16
17		content, err := io.ReadAll(file)
18		if err != nil {
19			panic("Could not read API key to string")
20		}
21
22		apiKey = string(content)
23	})
24}

And then we need to call this in the main function like so

1func main() {
2
3	initialiseApiKey()
4
5	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
6		fmt.Fprintf(w, "Hello world!")
7	})
8
9	fmt.Println("Starting the server!")
10	if err := http.ListenAndServe(":8080", nil); err != nil {
11		fmt.Println((err))
12	}
13}

apiKey is now globally available across threads to be used.

Creating a Naurt Request

Create a new file called types.go in the src folder - we’ll put all of the structs which handle the Naurt request in here.

$ touch src/types.go

We’ll start by typing the Naurt request, so the file only needs the following

1package main
2
3type NaurtRequest struct {
4	AddressString     string   `json:"address_string,omitempty"`
5	Latitude          *float64 `json:"latitude,omitempty"`
6	Longitude         *float64 `json:"longitude,omitempty"`
7	AdditionalMatches bool     `json:"additional_matches,omitempty"`
8}

Now that we have this type, we can create a simple function which will make this request for us. For now, our goal is to simply make the request and print out the JSON body we get back. We’ll define a function makeNaurtRequest which will handle actually making a request to Naurt. For now, it’ll be simple and we’ll just hard code the request in, but we can come back and customise that later on.

1func makeNaurtRequest() (string, error) {
2	url := "https://api.naurt.net/final-destination/v1"
3
4	data := NaurtRequest{
5		AddressString: "The Grand Hotel, Brighton",
6	}
7
8	jsonData, err := json.Marshal(data)
9	if err != nil {
10		return "", err
11	}
12
13	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
14	if err != nil {
15		return "", err
16	}
17
18	req.Header.Set("Content-Type", "application/json")
19	req.Header.Set("Authorization", apiKey)
20
21	client := &http.Client{}
22	resp, err := client.Do(req)
23	if err != nil {
24		return "", err
25	}
26
27	defer resp.Body.Close()
28
29	body, err := io.ReadAll(resp.Body)
30	if err != nil {
31		return "", err
32	}
33
34	return string(body), err
35}

It will also be easier if we write a dedicated handler function

1func handler(w http.ResponseWriter, r *http.Request) {
2	resp, err := makeNaurtRequest()
3	if err != nil {
4		fmt.Fprintf(w, err.Error())
5		return
6	}
7
8	fmt.Fprintf(w, resp)
9
10}

And then finally we can update the main function to use this new handler

1func main() {
2
3	initialiseApiKey()
4
5	http.HandleFunc("/", handler)
6
7	fmt.Println("Starting the server!")
8	if err := http.ListenAndServe(":8080", nil); err != nil {
9		fmt.Println((err))
10	}
11}

Go ahead and compile and run the code again - you should see a JSON print out when you go to localhost:8080/

Creating a Naurt Response

If we want to take this further, we’ll need to convert the JSON response we get from Naurt into data that we can work with more effectively in Go. To do this, we’ll use structs like the NaurtRequest to deserialise the response into. I’ll put the following types into types.go

1
2type NaurtResponse struct {
3	BestMatch         *DestinationResponse   `json:"best_match,omitempty"`
4	AdditionalMatches *[]DestinationResponse `json:"additional_matches,omitempty"`
5	Version           string                 `json:"version,omitempty"`
6}
7
8type DestinationResponse struct {
9	ID       string       `json:"id"`
10	Address  string       `json:"address"`
11	Geojson  NaurtGeojson `json:"geojson"`
12	Distance float32      `json:"distance,omitempty"`
13}
14
15type NaurtGeojson struct {
16	Features []Feature `json:"features"`
17	TypeVal  string    `json:"type"`
18}
19
20type Feature struct {
21	Geometry   Coordinates `json:"geometry"`
22	TypeVal    string      `json:"type"`
23	Properties Properties  `json:"properties"`
24}
25
26type Coordinates struct {
27	Coordinates CoordinatesWrapper `json:"coordinates"`
28	TypeVal     string             `json:"type"`
29}
30
31type CoordinatesWrapper struct {
32	Number       [][]float32
33	NestedNumber [][][]float32
34}
35
36type Properties struct {
37	NaurtType    string   `json:"naurt_type"`
38	Contributors []string `json:"contributors"`
39}

This is a fairly straightforward JSON deserialisation task, however there’s a few points worth looking at in more detail. I called all the type fields from the JSON TypeVal  since type is already a keyword in Go. Thankfully, Go makes it really easy to specify the name in the JSON.

The other point is CoordinatesWrapper - notice that it does not have a json marking. This is because Naurt can respond with two kinds of coordinates

  1. A double nested array. This represents a multipoint type, for example, the naurt_doors will be of this type.
  2. A triple nested array. This represents a multipolygon type, for example, the naurt_parking and naurt_building are of this type

However, both will not be present at once. Therefore, we need to write a small piece of custom deserlisation code for this

1func (f *CoordinatesWrapper) UnmarshalJSON(data []byte) error {
2
3	var doubleArray [][]float32
4	if err := json.Unmarshal(data, &doubleArray); err == nil {
5		f.Number = doubleArray
6		return nil
7	}
8
9	var tripleArray [][][]float32
10	if err := json.Unmarshal(data, &tripleArray); err == nil {
11		f.NestedNumber = tripleArray
12		return nil
13	}
14
15	return errors.New("`CoordinatesWrapper` did not find valid format")
16}

Note that you will need to import encoding/json and errors .

The point of this custom deserialise code is the struct will either have a double nested array or a triple nested array, while the other is nil and we can check for this at run time.

Now, we’ll edit the makeNaurtRequest function to return a NaurtRequest rather than a string

1func makeNaurtRequest() (NaurtResponse, error) {
2	url := "https://api.naurt.net/final-destination/v1"
3
4	data := NaurtRequest{
5		AddressString: "The Grand Hotel, Brighton",
6	}
7
8	jsonData, err := json.Marshal(data)
9	if err != nil {
10		return NaurtResponse{}, err
11	}
12
13	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
14	if err != nil {
15		return NaurtResponse{}, err
16	}
17
18	req.Header.Set("Content-Type", "application/json")
19	req.Header.Set("Authorization", apiKey)
20
21	client := &http.Client{}
22	resp, err := client.Do(req)
23	if err != nil {
24		return NaurtResponse{}, err
25	}
26
27	defer resp.Body.Close()
28
29	body, err := io.ReadAll(resp.Body)
30	if err != nil {
31		return NaurtResponse{}, err
32	}
33
34	var naurt NaurtResponse
35	if err := json.Unmarshal([]byte(body), &naurt); err != nil {
36		return NaurtResponse{}, err
37	}
38
39	return naurt, err
40}

We’ll also temporarily update the handler function to deal with this new return type. For now, we’ll just convert it back into a JSON and print it out. Although it seems like a lot of work to do nothing, we will very soon do something much more useful with the response

1func handler(w http.ResponseWriter, r *http.Request) {
2	resp, err := makeNaurtRequest()
3	if err != nil {
4		fmt.Fprintf(w, err.Error())
5		return
6	}
7
8	jsonBody, err := json.Marshal(resp)
9	if err != nil {
10		fmt.Fprintf(w, err.Error())
11		return
12	}
13
14	fmt.Fprintf(w, string(jsonBody))
15
16}

You should see the JSON again, but this time it has first been processed as a Go object we can work with.

Using URL Arguments

At the moment, we’ve hardcoded the request into the program. This is obviously much less useful than having the request be dynamic. The design we’ll use is parsing arguments out of the URL. Editing the makeNaurtRequest function is really straightforward

1func makeNaurtRequest(address string, latitude *float64, longitude *float64) (NaurtResponse, error) {
2	url := "https://api.naurt.net/final-destination/v1"
3
4	data := NaurtRequest{
5		AddressString:     address,
6		Latitude:          latitude,
7		Longitude:         longitude,
8		AdditionalMatches: true,
9	}
10	...

Make sure you import strcnov, then we can edit the handler to look like so

1func handler(w http.ResponseWriter, r *http.Request) {
2
3	query := r.URL.Query()
4
5	address := query.Get("address")
6
7	var latitude *float64
8	if query.Has("latitude") {
9		tmp, err := strconv.ParseFloat(query.Get("latitude"), 64)
10		if err != nil {
11			fmt.Fprintf(w, err.Error())
12			return
13		}
14		latitude = &tmp
15	}
16
17	var longitude *float64
18	if query.Has("longitude") {
19		tmp, err := strconv.ParseFloat(query.Get("longitude"), 64)
20		if err != nil {
21			fmt.Fprintf(w, err.Error())
22			return
23		}
24		longitude = &tmp
25	}
26
27	resp, err := makeNaurtRequest(address, latitude, longitude)
28	if err != nil {
29		fmt.Fprintf(w, err.Error())
30		return
31	}
32
33	jsonBody, err := json.Marshal(resp)
34	if err != nil {
35		fmt.Fprintf(w, err.Error())
36		return
37	}
38
39	fmt.Fprintf(w, string(jsonBody))
40
41}
42

Since address is meant to be a string we can just get it from the query - if it isn’t present we will simply get the empty string, which the JSON serialisation will ignore. However, we can now see why the latitude and longitude have been *float64 so far. If these arguments are not provided, we need them to be an empty type so they won’t serialise. If we can’t parse the arguments given in latitude or longitude as floats, we return an error, though.

Now if you compile and run the code, and use a search query, you’ll get different results! For example, try http://localhost:8080/?address=The Grand Hotel, Eastbourne

Plotting Naurt on a Map

At this point, if we were using Naurt purely in a backend application we’d be more or less done - this could be used in a route planning or ETA system as is. However, we’ll plot the output on a map just so we can visually see the results.

To do the plotting, we’ll make a new file called plotting.go

$ touch src/plotting.go

For this application we’ll be using Plotly, and there’s actually already a Plotly package in Go. We’ll need to install it though, which we can do with

$ go get github.com/MetalBlueberry/go-plotly

Inside plotting.go make sure to add package main to the top.

We’ll start by making some helper functions. Plotly typically expects all the latitudes to be grouped into one slice and all the longitudes grouped into another slice. Naurt follows the geojson standard, so the latitudes and longitudes are grouped into a single slice representing a point. We can create the following very simple functions to convert between the two

1func extractLats(points [][]float32) []float32 {
2	lats := []float32{}
3
4	for _, point := range points {
5		lats = append(lats, point[1])
6	}
7
8	return lats
9}
10
11func extractLons(points [][]float32) []float32 {
12	lons := []float32{}
13
14	for _, point := range points {
15		lons = append(lons, point[0])
16	}
17
18	return lons
19}

All of the useful data to do with plotting is found inside the DestinationResponse struct, so let’s write a function which can convert the DestinationResponse into something Plotly can work with.

1func extractNaurtInner(data *DestinationResponse, traces *[]grob.Trace, layout **grob.Layout, bestMatch bool) {
2	for _, feat := range data.Geojson.Features {
3		if feat.Geometry.Coordinates.Number != nil {
4			// naurt_door
5
6			if bestMatch {
7				*layout = &grob.Layout{
8					Mapbox: &grob.LayoutMapbox{
9						Style: "carto-positron",
10						Center: &grob.LayoutMapboxCenter{
11							Lat: float64(feat.Geometry.Coordinates.Number[0][1]),
12							Lon: float64(feat.Geometry.Coordinates.Number[0][0]),
13						},
14						Zoom: 15.0,
15					},
16					Showlegend: grob.False,
17					Autosize:   grob.True,
18				}
19			}
20
21			trace := &grob.Scattermapbox{
22				Type:   "scattermapbox",
23				Lat:    extractLats(feat.Geometry.Coordinates.Number),
24				Lon:    extractLons(feat.Geometry.Coordinates.Number),
25				Text:   fmt.Sprintf("%s - %s", data.Address, feat.Properties.NaurtType),
26				Marker: &grob.ScattermapboxMarker{Size: 9.0},
27			}
28
29			*traces = append(*traces, trace)
30
31		} else if feat.Geometry.Coordinates.NestedNumber != nil {
32			// naurt_parking or naurt_building
33
34			for _, shape := range feat.Geometry.Coordinates.NestedNumber {
35
36				trace := &grob.Scattermapbox{
37					Type: "scattermapbox",
38					Lat:  extractLats(shape),
39					Lon:  extractLons(shape),
40					Text: fmt.Sprintf("%s - %s", data.Address, feat.Properties.NaurtType),
41					Mode: grob.ScattermapboxModeLines,
42					Fill: grob.ScattermapboxFillToself,
43				}
44
45				*traces = append(*traces, trace)
46			}
47		}
48	}
49}
50

Make sure you import grob "[github.com/MetalBlueberry/go-plotly/graph_objects](<http://github.com/MetalBlueberry/go-plotly/graph_objects>)"  as well as "fmt" .

Plotly expects a slice of traces, so in the extractNaurtInner function, we pass in a reference to a slice of these traces that we’ll make outside the function and then continually append to. We also need a layout for a Plotly map. The layout contains the map centre. We’ll centre on the door of the best match from Naurt, which is why we need to tell this function whether it’s a best match or not. Again, we’ll pass in a pointer to this layout object and set it inside the function.

Speaking of a caller function, we can create the plotNaurt function now

1func plotNaurt(response NaurtResponse) (string, error) {
2
3	traces := []grob.Trace{}
4
5	var layout *grob.Layout
6
7	if response.BestMatch != nil {
8		extractNaurtInner(response.BestMatch, &traces, &layout, true)
9	}
10
11	if response.AdditionalMatches != nil {
12		for _, data := range *response.AdditionalMatches {
13			extractNaurtInner(&data, &traces, &layout, false)
14		}
15
16	}
17
18	if layout == nil {
19		return "", errors.New("no best match found")
20	}
21
22	fig := &grob.Fig{
23		Data:   traces,
24		Layout: layout,
25	}
26
27	jsonFig, err := json.Marshal(fig)
28	if err != nil {
29		return "", err
30	}
31
32	return string(jsonFig), nil
33
34}

Make sure you import "encoding/json" and "errors" here.

Essentially, we use the Plotly package to convert Naurt into a map. We’ll actually return a JSON as a string from this function, since in order to serve this to the user we’ll place it inside a template.

Templating the Response

We’re going to use templates the serve the response to the user. Let’s make a folder and template file for this purpose

$ mkdir templates
$ touch templates/index.html

Inside index.html place the following template code

1<!DOCTYPE html>
2<html>
3<head>
4  <meta charset="UTF-8">
5  <title>Plotly Map</title>
6  <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
7</head>
8<style>
9.container {
10    width: 100%;
11    height: 100vh;
12}
13</style>
14<body>
15  <div class="container" id="mapDiv"></div>
16  <script>
17    var data = JSON.parse({{.Data}});
18
19    console.log(data)
20
21    Plotly.newPlot("mapDiv", data.data, data.layout);
22  </script>
23</body>
24</html>

The basic idea is we’re going to template in a JSON, which will be parsed into an object and then contain the necessary data for creating the Plotly plot. Unfortunately, the Plotly package in Go doesn’t have some kind of function which converts the plot to HTML in memory (it does have one that converts it to a file, and while I didn’t benchmark I assume this is faster than then reading that file off the server disc and sending it).

Back in main.go we can add this near the top of the file, outside any function. Make sure to import html/template (not text/template which is very similar but won’t work!)

var tmpl *template.Template

tmpl will store the template which we can use over and over again. We’ll need to initialise it somewhere and the main function is the most natural

1func main() {
2
3	initialiseApiKey()
4
5	tmpl = template.Must(template.ParseFiles("templates/index.html"))
6
7	http.HandleFunc("/", handler)
8
9	fmt.Println("Starting the server!")
10	if err := http.ListenAndServe(":8080", nil); err != nil {
11		fmt.Println(err)
12	}
13}

We’ll create a small helper type which will pass the data to the template

1type PageData struct {
2	Data string
3}

Now all we have to do is update the handler function to actually use the template

1func handler(w http.ResponseWriter, r *http.Request) {
2
3	query := r.URL.Query()
4
5	address := query.Get("address")
6
7	var latitude *float64
8	if query.Has("latitude") {
9		tmp, err := strconv.ParseFloat(query.Get("latitude"), 64)
10		if err != nil {
11			fmt.Fprintf(w, err.Error())
12			return
13		}
14		latitude = &tmp
15	}
16
17	var longitude *float64
18	if query.Has("longitude") {
19		tmp, err := strconv.ParseFloat(query.Get("longitude"), 64)
20		if err != nil {
21			fmt.Fprintf(w, err.Error())
22			return
23		}
24		longitude = &tmp
25	}
26
27	resp, err := makeNaurtRequest(address, latitude, longitude)
28	if err != nil {
29		fmt.Fprintf(w, err.Error())
30		return
31	}
32
33	mapJson, err := plotNaurt(resp)
34	if err != nil {
35		fmt.Fprintf(w, err.Error())
36		return
37	}
38
39	data := PageData{Data: mapJson}
40
41	e := tmpl.Execute(w, data)
42	if e != nil {
43		fmt.Fprintf(w, e.Error())
44	}
45}

And we’re done! If we compile and run this code again, then we’ll get a map when we go to for example http://localhost:8080/?address=The Grand Hotel, Brighton

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.