Address validation and geocoding in Go

About 8 minutes. Updated 18 September 2026.

Install the Go client and resolve a typed address to a G-NAF id and a coordinate on the server, with near misses to offer back when nothing matches and typed errors for quota and rate limits.

The Go client is for the server: validating an address somebody typed, filling a coordinate on an order, or cleaning a table of addresses you already hold. It has no dependencies beyond the standard library.

01Install it

sh
go get github.com/locio-au/locio-go@latest

Go 1.22 or newer. The import path is github.com/locio-au/locio-go and the package name is locio.

02Resolve what somebody typed

/v1/addresses/resolve takes a line of text and answers whether it is a real address, which one it is, and where it is, in one call.

main.gogo
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/locio-au/locio-go"
)

func main() {
	// A secret key. This runs on your server, never in a browser.
	//
	// WithCountry says which register to search. Leave it off and the
	// service searches AU, which is the only one with data today; your
	// server's own location is never consulted.
	c := locio.New(os.Getenv("LOCIO_API_KEY"), locio.WithCountry("AU"))

	res, err := c.Resolve(context.Background(), "1 george st sydenham nsw 2044")
	if err != nil {
		log.Fatal(err)
	}

	if !res.Matched {
		fmt.Println("no match")
		return
	}

	fmt.Println(res.Address.Formatted)
	fmt.Println(res.Address.ID)
	fmt.Println(res.Address.Lat, res.Address.Lng)
}

Store ID. It is the address's id in its country's register, which in Australia is the G-NAF Address Detail PID, sent under address_detail_pid as well. It is stable across releases for an address that has not changed, where the formatted line is not, so it is the thing to key your own records on rather than the text. AddressDetailPID carries the same value on an Australian record, for code that wants to say plainly that it is holding a G-NAF pid.

03Offer near misses when nothing matches

A typo should not become a dead end. /v1/addresses/similar finds the nearest real addresses so you can show them as options.

main.gogo
// Nothing matched. Offer the nearest real addresses back rather
// than telling somebody their address does not exist.
near, err := c.Similar(ctx, typed, 5)
if err != nil {
	return err
}

for _, a := range near {
	fmt.Println(a.Formatted, a.ID)
}

04Handle the errors you will actually see

Failures come back as a typed *locio.Error carrying the status, so quota, rate limiting and a revoked key are all things you can branch on rather than strings you have to match.

main.gogo
res, err := c.Resolve(ctx, typed)

var apiErr *locio.Error
if locio.AsError(err, &apiErr) {
	switch apiErr.Status {
	case 402:
		// Out of quota for the month.
	case 429:
		// Rate limited. Back off and retry.
	case 401:
		// The key is wrong, or revoked.
	}
}

if locio.IsNotFound(err) {
	// An id that is not in the current release, or one that belongs to a
	// country this key is not searching.
}

What it costs

One unit per request. A resolve is one call, and the near misses only cost another when the first one did not match. Refused requests are free.

Related