Validate and geocode Australian addresses in PHP
About 15 minutes. Updated 11 September 2026.
A small PHP client for the Locio API that checks whether an address is real, returns its G-NAF id and coordinates, and falls back to fuzzy matching for typos. Plain curl, no framework required, with a Laravel variant.
On the server the question is different from autocomplete. You already hold an address, from a checkout form, a CRM or a spreadsheet, and want to know whether it is real, where it is, and what its parts are. /v1/addresses/resolve answers all three in one call, because they are one question.
Use a secret key here, read from the environment. A secret key can call every endpoint and works from anywhere, so it never goes in a page; that is what public keys are for.
01A small client
Plain curl, which every PHP install has. Errors come back as RFC 7807 problem documents with a detail written for a person, so the exception carries that sentence and the status.
<?php
// src/Locio.php
declare(strict_types=1);
final class Locio
{
private const API = 'https://api.locio.com.au';
public function __construct(private readonly string $key) {}
/**
* The address as someone typed or stored it, in one string. Returns the
* resolved address, or null when G-NAF has no such address.
*/
public function resolve(string $address): ?array
{
$data = $this->get('/v1/addresses/resolve', ['q' => $address]);
return $data['matched'] ? $data['address'] : null;
}
/** Close matches for an address with typos in it. Three units a call. */
public function similar(string $address, int $limit = 5): array
{
return $this->get('/v1/addresses/similar', ['q' => $address, 'limit' => $limit]);
}
/** An address read back by the id you stored. */
public function byId(string $gnafPid): ?array
{
try {
return $this->get('/v1/addresses/' . rawurlencode($gnafPid));
} catch (LocioException $e) {
if ($e->status === 404) return null; // G-NAF retired the id; search again
throw $e;
}
}
private function get(string $path, array $query = []): array
{
$url = self::API . $path . ($query ? '?' . http_build_query($query) : '');
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $this->key, 'Accept: application/json'],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($body === false) {
throw new LocioException(0, "could not reach the API: $error");
}
$json = json_decode($body, true);
if ($status >= 400) {
// RFC 7807: the detail is written for a person to read.
throw new LocioException($status, $json['detail'] ?? $json['title'] ?? "the API answered $status");
}
return $json['data'];
}
}
final class LocioException extends RuntimeException
{
public function __construct(public readonly int $status, string $message)
{
parent::__construct($message);
}
}02Validate, geocode, parse
matched says whether the address exists in G-NAF. When it does, the answer carries the id, the coordinates and every component. When it does not, the usual reason is a typo, and /v1/addresses/similar finds the nearest real addresses to offer back.
<?php
require 'src/Locio.php';
$locio = new Locio(getenv('LOCIO_KEY')); // a secret key, kept on the server
$address = $locio->resolve('1 george st sydenham nsw 2044');
if ($address === null) {
// Not a real address as written. Offer the nearest ones instead.
foreach ($locio->similar('1 george st sydenham nsw 2044') as $candidate) {
echo $candidate['formatted'], PHP_EOL;
}
exit;
}
// Validated, geocoded and parsed, from one call.
echo $address['gnaf_pid'], PHP_EOL; // store this against your record
echo $address['lat'], ', ', $address['lng'], PHP_EOL; // straight onto a map
echo $address['components']['postcode'], PHP_EOL;
echo $address['mesh_block'] ?? '', PHP_EOL; // the ABS census join key03Handle the two refusals worth handling
402 means the period's units are spent, 429 means too many requests in one second. Both are cheap to tell apart by status, and neither is charged.
try {
$address = $locio->resolve($input);
} catch (LocioException $e) {
match ($e->status) {
402 => $log->warning('Locio quota is spent until the period resets'),
429 => usleep(250_000), // one plan's requests a second; back off and retry
default => $log->error($e->getMessage()),
};
}04In Laravel
// Laravel: the same calls through the Http facade.
use Illuminate\Support\Facades\Http;
$res = Http::withToken(config('services.locio.key'))
->timeout(5)
->get('https://api.locio.com.au/v1/addresses/resolve', ['q' => $input]);
$data = $res->throw()->json('data');
$address = $data['matched'] ? $data['address'] : null;05Cleaning a file
The same client, once per row. A thousand rows is a thousand units, which the free tier covers ten times over.
<?php
// Clean a spreadsheet: one line in, one line out, with the id and coordinates.
$in = fopen('customers.csv', 'r');
$out = fopen('customers-resolved.csv', 'w');
fputcsv($out, ['input', 'matched', 'gnaf_pid', 'formatted', 'lat', 'lng']);
while (($row = fgetcsv($in)) !== false) {
$address = $locio->resolve($row[0]);
fputcsv($out, [
$row[0],
$address ? 'yes' : 'no',
$address['gnaf_pid'] ?? '',
$address['formatted'] ?? '',
$address['lat'] ?? '',
$address['lng'] ?? '',
]);
}Related
- Address autocomplete in plain JavaScript
- Address autocomplete in React
- Address autocomplete in Vue
- Stand up a React site with Australian address search
- Put a searched address on a map with MapLibre
- Use Locio from Claude Code, Claude Desktop and Cursor
- Give an agent address tools with the Anthropic API
- Clean a spreadsheet of addresses with an agent
- The API reference, with every call in curl, Node, Python, Ruby, Go and PHP.