Australian address autocomplete and verification in Python
WattleAddr is an address API for Australian street addresses. The data is G-NAF, the national address file, and the API is hosted in Australia. This guide shows the calls in Python: search as someone types, retrieve the address they pick, verify an address you already hold, find the nearest addresses to a point, check a form on your server, and handle errors. For the full quickstart, see the documentation.
Before you start
- Create a secret key in the console and set it as
WATTLEADDR_KEY. In a terminal, export WATTLEADDR_KEY=your_secret_key. - Tested on Python 3.12. You need the requests package: pip install requests.
- Secret keys stay on your server. Never put one in a web page or an app. For a type-ahead in the browser, use the widget or
@wattleaddr/jswith a publishable key. See Add it to your site. - Test keys are never charged. They stop working 15 days after your account was created (
402 test_mode_expired).
Search and retrieve
Two calls make up one lookup. autocomplete(q, session) returns suggestions as the person types, and retrieve(id, session) returns the full address for the one they pick. Use the same session for both. Keystrokes are fair use and are not billed on their own. One session is one lookup, charged when you retrieve. A retrieve without a session is charged on its own. This is a small wattleaddr.py module that the other files import.
# wattleaddr.py. Tested on Python 3.12, with requests (pip install requests).
# Reads your secret key from the WATTLEADDR_KEY environment variable.
import os
import time
import uuid
from urllib.parse import quote
import requests
API = "https://api.wattleaddr.com.au"
class WattleAddrError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status = status
self.code = code
self.message = message
def _request(method, path, retry=True, **kwargs):
key = os.environ.get("WATTLEADDR_KEY")
if not key:
raise WattleAddrError(0, "missing_key", "Set WATTLEADDR_KEY to your secret key.")
headers = {"Authorization": "Bearer " + key}
r = requests.request(method, API + path, headers=headers, timeout=10, **kwargs)
if r.ok:
return r.json()
try:
err = r.json().get("error", {})
except ValueError:
err = {}
if retry and r.status_code in (429, 503):
# Wait as long as the API asks, then try once more.
wait = r.headers.get("Retry-After", "1")
time.sleep(min(int(wait) if wait.isdigit() else 1, 30))
return _request(method, path, retry=False, **kwargs)
raise WattleAddrError(r.status_code, err.get("code"), err.get("message"))
def new_session():
# One session per address. Use it for every keystroke and for the retrieve.
return str(uuid.uuid4())
def autocomplete(q, session):
body = _request("GET", "/v1/addresses/autocomplete", params={"q": q, "session": session})
return body["suggestions"]
def retrieve(address_id, session):
return _request("GET", "/v1/addresses/" + quote(address_id, safe=""), params={"session": session})
Then use it like this.
# search.py. Run: WATTLEADDR_KEY=your_secret_key python3 search.py
from wattleaddr import new_session, autocomplete, retrieve
session = new_session()
# Call this as the person types, from 3 characters. Keystrokes are fair use.
suggestions = autocomplete("1 martin place sydney", session)
for s in suggestions:
print(s["id"], s["formatted"])
if not suggestions:
raise SystemExit("No suggestions")
# Retrieve the one they picked, with the same session. This is the lookup that is charged.
picked = suggestions[0]
address = retrieve(picked["id"], session)
print(address["formatted"], address["components"]["postcode"], "billed:", address["billed"])
In your app, retrieve the suggestion the person picked, not the first one. Suggestions start from three characters. PO Boxes are not supported, because G-NAF holds physical addresses only.
Verify an address you already hold
Use POST /v1/addresses/verify for free text you already have, such as an address from a form, a CRM or a spreadsheet. Send JSON with address and a session. It needs a secret key, so call it from your server only. A verify that finds no match is not billed.
Do not verify an address the person just picked from autocomplete. It is already a G-NAF record, and you paid for it when you retrieved it.
Add this to the same file.
# Add to wattleaddr.py.
def verify(address, session):
# Secret key only. Do not call this for an address the person just picked from autocomplete.
return _request("POST", "/v1/addresses/verify", json={"address": address, "session": session})
def check_address(text):
result = verify(text, new_session())
verdict = result["verdict"] # never branch on the deprecated "confidence"
match = result["match"]
if verdict in ("verified", "corrected") and match:
return {
"verdict": verdict,
"ok": True,
"address": match,
# For corrected, show this to the person. Spelling or abbreviations may have been fixed.
"show": match["formatted"] if verdict == "corrected" else None,
# Parts we replaced, if any. Spelling fixes are not listed here.
"changed": result["changed_elements"],
}
# The match is one of the choices. alternatives does not include it.
options = ([match] if match else []) + result["alternatives"]
if verdict == "ambiguous" and options:
return {"verdict": verdict, "ok": False, "ask": "pick", "options": options}
return {"verdict": verdict, "ok": False, "ask": "check"}
Branch on verdict. The older confidence field is deprecated.
verdict | What to do |
|---|---|
verified | Accept match. |
corrected | Accept match. Show the person match.formatted, because spelling or abbreviations may have been fixed. changed_elements lists any part we replaced. |
ambiguous | Do not accept it automatically. Ask the person to pick from match and alternatives. |
unverified | Ask the person to check the address. There is usually no match. |
Try it on four addresses, one for each verdict.
# verify_demo.py. Run: WATTLEADDR_KEY=your_secret_key python3 verify_demo.py
from wattleaddr import check_address
for text in ["35 Bergin Circuit Leppington NSW 2179", "35 Bergin Cct Leppingtn NSW 2179", "1 martin place sydney nsw 2000", "1 martn pl sydny nsw 2000"]:
result = check_address(text)
if result["ok"]:
print(result["verdict"], "->", result["address"]["formatted"], result["changed"])
elif result["ask"] == "pick":
print(result["verdict"], "-> ask the person to pick one of", len(result["options"]))
for option in result["options"]:
print(" ", option["id"], option["formatted"])
else:
print(result["verdict"], "-> ask the person to check the address")
A verified address matches a G-NAF record. That is not a promise that mail can be delivered to it. The full list of response fields is in the verify documentation.
Reverse geocode a location
Use GET /v1/addresses/reverse to turn a point into the nearest addresses. Send lat and lng. It needs a secret key, so call it from your server only. The search is 100 metres by default, and limit is 1 by default. A call that returns no address is not billed. Add this to the same file.
# Add to wattleaddr.py.
def reverse_geocode(lat, lng, radius=100, limit=1):
# Secret key only, so call it from your server.
# Each call that finds an address is one lookup. A session does not make repeat calls free.
params = {"lat": lat, "lng": lng, "radius": radius, "limit": limit}
return _request("GET", "/v1/addresses/reverse", params=params)
Then use it like this.
# reverse_demo.py. Run: WATTLEADDR_KEY=your_secret_key python3 reverse_demo.py
from wattleaddr import reverse_geocode
# A point from your own server, such as a driver's last position.
result = reverse_geocode(-33.99168126, 150.80123469, radius=100, limit=3)
if not result["addresses"]:
raise SystemExit("No address within 100 metres")
for a in result["addresses"]:
print(a["formatted"], a["distance_m"], "m")
print("billed:", result["billed"])
Each address has a distance_m in metres, nearest first. Units often share their building’s coordinate, so the base address comes first on a tie. The coordinate is the G-NAF geocode, usually the property centroid. It is not the building’s door. Every call that returns an address is one lookup, and a session does not stop a repeat call from being billed. The parameters, billing and errors are in the reverse geocoding documentation. New to APIs? Read the step by step guide.
Check a form on your server
Here is a Flask handler that does it for a submitted form. It takes the submitted address, verifies it and returns the result as JSON. Your page reads the verdict, ok, ask and show fields. When show is set, show the person that address before you save it.
# app.py. Flask (pip install flask requests). Run: WATTLEADDR_KEY=your_secret_key flask --app app run
from flask import Flask, jsonify, request
from wattleaddr import WattleAddrError, check_address
app = Flask(__name__)
@app.post("/check-address")
def check():
text = request.form.get("address", "").strip()
if len(text) < 3:
return jsonify(ok=False, ask="check"), 400
try:
return jsonify(check_address(text))
except WattleAddrError as e:
app.logger.error("WattleAddr: %s", e)
# Do not send the API's message to the browser. Let the person carry on and try again.
return jsonify(ok=False, error="try_again"), 502
Post a form to it with curl -d "address=35 Bergin Circuit Leppington NSW 2179" and the JSON comes back with the verdict. Flask is only an example. The same check_address function works in any framework. Put it behind the same login, CSRF check and rate limit as the rest of your form. Anyone who can reach it can use your lookups.
Errors and retries
Every error has the same shape: {"error":{"code","message","docs"}}. Switch on code, not on the wording of message. The client above does two things for you. It retries once on 429 rate_limited and 503 service_unavailable, after the number of seconds in the Retry-After header. Any other error stops with the code the API sent.
401 unauthorized: no key was sent. Check thatWATTLEADDR_KEYis set.401 invalid_key: a key was sent, but it is wrong or revoked. Copy it again from the console.402 quota_exceeded: this month’s lookups are used up and your plan does not run over, or your plan has reached its overage ceiling.429 rate_limitedand503 service_unavailable: wait forRetry-After, then retry. If the retry also fails, tell the person to try again.
# errors_demo.py. Run: WATTLEADDR_KEY=your_secret_key python3 errors_demo.py
from wattleaddr import new_session, autocomplete, WattleAddrError
try:
autocomplete("1 martin place sydney", new_session())
except WattleAddrError as e:
# Switch on e.code, not on the wording of e.message.
if e.code == "unauthorized":
print("unauthorized: no key was sent. Set WATTLEADDR_KEY.")
elif e.code == "invalid_key":
print("invalid_key: the key is wrong or revoked. Copy it again from the console.")
elif e.code == "quota_exceeded":
# quota_exceeded comes from retrieve and verify. Autocomplete keeps working.
print("quota_exceeded: this month's lookups are used up. Check your plan in the console.")
elif e.code in ("rate_limited", "service_unavailable"):
print(e.code + ": still refused after one retry. Try again later.")
else:
raise
The full list of codes, with what each one means and what to do, is on the errors page.
Storing the result
Store the id, the formatted text and the components together. Ids can change between G-NAF releases, so keep the parts as well as the id. To refresh an address, search again. Do not replay an old id. Keep postcode as a string, because some postcodes start with a zero. There is a schema in how to store an Australian address. For prices, see pricing.