Skip to content

Repository files navigation

postfinder/postfinder

Post offices, parcel lockers and post boxes, as an API, for PHP. Ask what is nearest a coordinate, look up what a postcode covers, or read a suburb's locations.

  • Free and keyless. No account, no quota to buy, nothing to configure
  • No dependencies: ext-curl by default, or bring your own transport (WordPress, Laravel, any PSR-18 client) in a few lines
  • PHP 8.1 and up, with readonly value objects for the rows you use most
  • Australia, New Zealand, the United States, the United Kingdom and Canada
composer require postfinder/postfinder

Quick start

use PostFinder\Client;

$pf = new Client();

$near = $pf->nearby(-37.7404, 144.9633, 'post-offices', 'australia');

foreach ($near as $place) {
    echo $place->name, ', ', $place->address, ', ', $place->metres(), " m\n";
}

Nearest first, within 50km, at most 30 rows. That is the question "where do I post this", which is a different question from "list every post box in Victoria".

The six categories

Client::CATEGORIES;
// post-offices, post-boxes, express-post-boxes, parcel-lockers,
// drop-off-points, collection-points

A name outside the six throws InvalidArgumentException before any request goes out.

Suburb and place search

$hits = $pf->search('coburg', limit: 8);

foreach ($hits as $hit) {
    echo $hit->kind, ' ', $hit->name, ' ', $hit->postcode, ' ', $hit->path(), "\n";
}

Two characters minimum: below that it returns [] without asking, which is what the service answers anyway. Behind a search box, debounce by at least 150ms.

Linking back to the site

Every row carries the slugs its page path is built from, so no second request is needed:

use PostFinder\Paths;

$hit->path();                  // /en/australia/victoria/coburg/
$place->path();                // /en/australia/victoria/coburg/australia-post/coburg-post-office-uv5h25cs/
Paths::locality('australia', 'victoria', 'coburg');

A location's path carries its brand as a segment, and a place with no brand is filed under unbranded. A search row does not carry the brand, so a place hit links to its suburb; place() returns the exact path.

Postcodes

A postcode is not a suburb. 3058 is Coburg and Coburg North, and an address in either is written with the same four digits.

$detail = $pf->postcode('australia', '3058');
array_column($detail['localities'], 'name');   // ['Coburg', 'Coburg North']

The whole country comes in one response, and it is meant to be kept:

$index = $pf->postcodes('australia');   // one request
$index->suburbsIn('3058');              // no request, and no rescan

suburbsIn builds its map on first use and keeps it on the index, so resolving a column of ten thousand postcodes is one pass over the country rather than ten thousand walks through every state. Cache the index (APCu, Redis, a file) rather than fetching it per request.

A location, a suburb, a country

$detail = $pf->place('uv5h25cs');       // PlaceDetail: place, nearby, reviews, photos
$suburb = $pf->locality('australia', 'victoria', 'coburg');

$pf->countries();
$pf->country('australia');
$pf->region('australia', 'victoria', limit: 100, offset: 0);
$pf->category('australia', 'parcel-lockers', region: 'victoria');

A place's public id is permanent. It is minted once and never derived from a source record, so a feed that renumbers its rows does not change it. Store the id, not the name or the path.

SearchHit, NearbyPlace, Place and PlaceDetail are readonly objects, and each keeps every field the service sent in $raw, so a field added to the API later is readable before this package names it. The suburb, region, category and postcode calls return the decoded arrays, with their shapes documented for PHPStan and Psalm.

Errors

use PostFinder\Exception\NotFound;
use PostFinder\Exception\RateLimited;

try {
    $detail = $pf->place($storedId);
} catch (NotFound) {
    $detail = null;   // retired, or never there
}

NotFound is ordinary rather than a failure: a suburb with no locations has no page, and a location that closed is retired. Every error extends PostFinderException and carries the status, title and detail the service sent; RateLimited carries retryAfter when the service said. No answer at all (a timeout, a refused connection) is a PostFinder\Http\TransportException.

Your own transport

The client needs one method, so a framework's HTTP client fits behind it:

use PostFinder\Http\Response;
use PostFinder\Http\Transport;

final class WordPressTransport implements Transport
{
    public function get(string $url, array $headers): Response
    {
        $res = wp_remote_get($url, ['headers' => $headers, 'redirection' => 0, 'timeout' => 15]);
        if (is_wp_error($res)) {
            throw new \PostFinder\Http\TransportException($res->get_error_message());
        }

        return new Response(
            (int) wp_remote_retrieve_response_code($res),
            array_change_key_case((array) wp_remote_retrieve_headers($res)->getAll()),
            (string) wp_remote_retrieve_body($res),
        );
    }
}

$pf = new Client(transport: new WordPressTransport());

A transport must not follow redirects and must hand back every status rather than throwing on 4xx and 5xx. The default CurlTransport does both, verifies TLS, refuses any scheme but http and https, and stops reading past 8 MiB.

Being a good citizen

The API is free and asks for care in return: around a thousand requests a month from one address, answers kept rather than fetched again, typing debounced. If you need more, say what you are building at postfinder.io/en/contact/.

$pf = new Client(contact: 'https://example.com/about-our-bot');

Attribution

Locations come from OpenStreetMap (ODbL), localities and postcodes from GeoNames (CC BY 4.0). If you publish what you get back, you carry those credits with it. The sources page names each one.

Also available

PHP postfinder/postfinder
JavaScript and TypeScript @postfinder/client
React @postfinder/react
Vue @postfinder/vue
Python postfinder
Go postfinder-go

Licence

MIT.

About

PHP client for PostFinder: the nearest post offices, parcel lockers and post boxes, and postcode lookup. Free and keyless.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages