Search by

maeandrew / novaposhta-address-resolver

maeandrew

Framework-free Nova Poshta address resolution core

Package info

github.com/maeandrew/novaposhta-address-resolver

pkg:composer/maeandrew/novaposhta-address-resolver

Statistics

Installs: 12

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.4.2 2026-09-10 06:03 UTC

This package is auto-updated.

Last update: 2026-09-10 06:03:50 UTC


README

English · Українська

Standalone PHP tooling for turning a free-form Nova Poshta address into a validated settlement and warehouse selection.

The project is intentionally split into a framework-free resolver core and optional adapters. A user may connect a ready Nova Poshta SDK or a custom HTTP client, then add an AI provider without coupling the core to a vendor SDK.

Unofficial community project. It is not affiliated with Nova Poshta.

What it solves

Real order messages often contain variants such as:

Київ, відділення номер 285
м. Київ НП 285
Львів поштомат 12345

The resolver parses and normalizes the text, queries a location provider, scores candidates, and returns a result that is safe to persist:

resolved       exact settlement and warehouse selected
ambiguous      more than one plausible candidate; manual choice required
not_found      no candidate matched the requested address
provider_error external data source failed or returned invalid data
invalid_input  the input cannot be meaningfully parsed

The resolver never writes to an order or silently chooses a branch when the evidence is insufficient.

Installation

The framework-free core is installed with Composer:

composer require maeandrew/novaposhta-address-resolver

The core requires PHP 8.2 or newer and ext-mbstring. It has no Laravel, HTTP-client, Nova Poshta SDK, or AI SDK dependency.

Core usage

use MaeAndrew\NovaPoshtaAddressResolver\AddressResolver;
use MaeAndrew\NovaPoshtaAddressResolver\DTO\AddressInput;

$resolver = new AddressResolver($locationProvider);

$result = $resolver->resolve(
    AddressInput::fromText('Київ, відділення №285')
);

if ($result->isResolved()) {
    $settlementRef = $result->settlement?->ref;
    $warehouseRef = $result->warehouse?->ref;
} elseif ($result->needsReview()) {
    foreach ($result->candidates as $candidate) {
        // Show candidates to a person and persist only after an explicit choice.
    }
}

$safePayload = $result->toArray();

toArray() returns the status, normalized records, confidence, candidates, parsed fields, diagnostics, and provider name. Raw provider payloads are omitted unless toArray(includeRawPayload: true) is explicitly requested.

Provider extension

The application supplies the source of truth by implementing three small methods. A provider translates its SDK, HTTP client, or local fixture data into the core DTOs:

use MaeAndrew\NovaPoshtaAddressResolver\Contracts\LocationProvider;
use MaeAndrew\NovaPoshtaAddressResolver\DTO\ProviderHealth;
use MaeAndrew\NovaPoshtaAddressResolver\DTO\ProviderResult;
use MaeAndrew\NovaPoshtaAddressResolver\DTO\Settlement;
use MaeAndrew\NovaPoshtaAddressResolver\DTO\SettlementQuery;
use MaeAndrew\NovaPoshtaAddressResolver\DTO\Warehouse;
use MaeAndrew\NovaPoshtaAddressResolver\DTO\WarehouseQuery;

final class MyLocationProvider implements LocationProvider
{
    public function searchSettlements(SettlementQuery $query): ProviderResult
    {
        // Query the chosen source and return list<Settlement>.
        return new ProviderResult([]);
    }

    public function searchWarehouses(
        Settlement $settlement,
        WarehouseQuery $query,
    ): ProviderResult {
        // Query only warehouses belonging to $settlement->ref.
        return new ProviderResult([]);
    }

    public function healthCheck(): ProviderHealth
    {
        return ProviderHealth::healthy('my-provider');
    }
}

Provider failures should throw ProviderException or another clear exception; the resolver exposes them as provider_error instead of treating an outage as not_found. The provider must return normalized Settlement and Warehouse objects. It must not trust a reference supplied by an AI or by an unvalidated caller.

The built-in parser recognizes Ukrainian and common Russian forms such as м., відд., відділення, поштомат, пункт, , НП, and spoken warehouse numbers such as двісті вісімдесят п’ять. Matching is configurable:

use MaeAndrew\NovaPoshtaAddressResolver\DTO\ResolutionPolicy;
use MaeAndrew\NovaPoshtaAddressResolver\Matching\StrictMatchingStrategy;

$resolver = new AddressResolver(
    $locationProvider,
    matchingStrategy: new StrictMatchingStrategy(),
    policy: new ResolutionPolicy(
        autoResolveThreshold: 0.90,
        ambiguityMargin: 0.08,
    ),
);

BalancedMatchingStrategy is the default. StrictMatchingStrategy uses exact references, names, types, numbers, and address fields. SuggestMatchingStrategy returns ranked candidates while keeping the result reviewable.

AI is recommended for production input

For production free-form messages, AI should be treated as the primary quality layer. It handles spelling mistakes, mixed languages, omitted labels, and unusual formats that deterministic parsing cannot cover as well. The deterministic pipeline remains a safe degraded mode for outages, privacy- restricted inputs, offline jobs, and tests, so the core package does not force an AI SDK or API key.

The core AI contracts and a fake provider are included in the core package. An AI suggestion may only rank provider candidates; it can never create or persist a settlement or warehouse reference. Configure a fallback chain when more than one provider is available.

The optional OpenAI adapter uses the Responses API and structured JSON output:

# after publishing the adapter package
composer require maeandrew/novaposhta-address-resolver-openai

During monorepo development, install its local dependencies with ddev exec bash scripts/install-openai.sh. The adapter is kept in a separate package so the core's Composer install remains small.

It accepts PSR HTTP client and factory implementations from the host application, so the core and adapter remain independent of a particular HTTP client. The adapter is wired through StructuredAddressAiInterpreter:

use MaeAndrew\NovaPoshtaAddressResolver\AI\StructuredAddressAiInterpreter;
use MaeAndrew\NovaPoshtaAddressResolver\OpenAI\OpenAiStructuredAiProvider;

$structuredProvider = new OpenAiStructuredAiProvider(
    $httpClient,
    $requestFactory,
    $streamFactory,
    $_ENV['OPENAI_API_KEY'],
);
$resolver = new AddressResolver(
    $locationProvider,
    aiInterpreter: new StructuredAddressAiInterpreter($structuredProvider),
);

The adapter is optional and its tests use a fake HTTP client; CI never calls an AI endpoint.

Optional Laravel bridge

Laravel integration is a separate package and does not add framework classes to the core:

composer require maeandrew/novaposhta-address-resolver-laravel

It provides service-container bindings, configurable cache, resolution events, a queue job, and novaposhta:resolve. The host application binds its own LocationProvider and decides how resolved references are persisted. See the Laravel integration guide.

The bridge source is published in its own GitHub repository and is available on Packagist.

During monorepo development, use ddev exec bash scripts/install-laravel.sh to install the bridge against the local core package.

Security and limitations

  • The core never writes to orders, customers, databases, or caller-owned data.
  • No live API calls are made by the test suite or CI; tests use synthetic fixture-* records.
  • Raw input and raw provider payloads are not included in result diagnostics by default. Applications should redact personal data before optional AI use.
  • Fuzzy matching is a suggestion mechanism. A result stays ambiguous when the top candidates are too close or the configured threshold is not met.
  • Provider data freshness, authentication, retries, rate limits, and caching remain responsibilities of the adapter or host application.

Development

For consistent local development, this repository includes a DDEV configuration with PHP and Composer:

ddev start
ddev composer install
ddev composer quality

The quality command runs PHP CS Fixer, PHPStan, and the complete PHPUnit suite. CI runs the same checks on PHP 8.2 and 8.3.

The optional Laravel bridge has its own local dependency setup and quality suite:

ddev exec bash scripts/install-laravel.sh
ddev composer quality --working-dir=packages/laravel

Documentation

  • AGENTS.md — handoff rules for implementation agents.
  • docs/IMPLEMENTATION_PLAN.md — detailed scope, contracts, algorithm, milestones, and acceptance criteria.
  • docs/AI_PROVIDERS.md — provider abstraction, adapters, fallback, and privacy.
  • docs/ARCHITECTURE.md — core, provider, AI, and Laravel boundaries.
  • docs/LARAVEL.md — optional Laravel bridge, queue, events, cache, and mapping.
  • examples/ — synthetic provider, AI, and address fixtures; no customer data.

License

Use MIT unless the project owner selects another permissive license before the first public release.