← All posts Developers

How to store an Australian address

1 Aug 2026 · 7 min read

Most Australian address bugs start the same way: a single `address` column of free text, filled by whatever the customer typed. It works until you need to do anything with it — group orders by suburb, work out a delivery zone, dedupe two records that are the same house, or hand a dispatch system something it can parse. By then the bad data is years deep.

Store components, not a sentence

An Australian address has a defined structure, and G-NAF — the national address file — records it as separate parts. These are the components WattleAddr returns, and they are a sound basis for your own schema:

{
  "flat_number":   "12",
  "level_number":  "3",
  "street_number": "45",
  "street_name":   "MARTIN",
  "street_type":   "PLACE",
  "locality":      "SYDNEY",
  "state":         "NSW",
  "postcode":      "2000"
}

Keep them apart in your database. Concatenating a unit into the street number ("12/45") is easy to do and painful to undo, and every downstream system that wants the unit separately — couriers especially — will have to guess.

Postcode is a string. Always.

This is the single most common Australian address bug in code written elsewhere. Northern Territory postcodes start with a zero — Darwin is 0800. Store a postcode as an integer and it becomes 800, and it will fail every validation and lookup from then on. It is a four-character string, and it is not arithmetic; nobody adds two postcodes together.

State is a fixed, short list

Eight values, and they never change: NSW, VIC, QLD, SA, WA, TAS, NT, ACT. Make it an enum or a check constraint. A free-text state column will accumulate "New South Wales", "N.S.W.", "nsw " and "NSW " until someone writes a cleanup script.

Street numbers are not numbers either

Real Australian street numbers include 12A, 12-14, and Lot 5. Store the number as text. The same goes for unit and level: "G" is a real level, and so is "LG".

Keep three things, not one

For anything you have matched against G-NAF, store all three of these — they answer different questions:

  • The stable id of the matched record. This is the key you re-fetch by, and the only reliable way to say "these two customers live at the same address" without string comparison.
  • The formatted string, for display and for printing on labels, so what the customer sees is what you matched.
  • The components, for anything your code has to reason about: shipping zones, tax, catchments, grouping by suburb.

Also record which G-NAF release you matched against. Geoscape publishes a new one roughly quarterly, and every WattleAddr response tells you the release it came from. Storing it means you can answer "when was this address last confirmed to exist?" — a question that becomes interesting the first time a customer insists their new estate is real.

A schema to copy

create table addresses (
  id              uuid primary key default gen_random_uuid(),
  gnaf_id         text,           -- the matched record; null if unverified free text
  formatted       text not null,  -- what to display
  flat_number     text,
  level_number    text,
  street_number   text,           -- text: "12A", "12-14"
  street_name     text,
  street_type     text,
  locality        text,
  state           text check (state in ('NSW','VIC','QLD','SA','WA','TAS','NT','ACT')),
  postcode        text check (postcode ~ '^[0-9]{4}$'),   -- string, leading zeros intact
  latitude        numeric(9,6),
  longitude       numeric(9,6),
  gnaf_release    text,           -- which quarterly release confirmed it
  verified_at     timestamptz,
  postal_address  text            -- PO Box etc., kept separate on purpose
);

Leave room for what G-NAF cannot hold

G-NAF is a register of physical addresses. It contains no PO Boxes, GPO Boxes, Locked Bags or Private Bags, so no G-NAF-based API can validate one. If your customers post to a PO Box, that belongs in its own nullable column beside the physical address, not squeezed into the street fields where it will fail validation forever.

Don’t write your own parser

The tempting next step is a regex that splits free text into these fields. It will handle the first thousand addresses and then meet "Shop 4, 12-14 The Corso", or a rural property with no street number, and quietly put the wrong thing in the wrong column. Matching against the national dataset is the job an address API exists to do — and it returns the components already separated, which is the whole point of this article.

Get an API key Read the docs