Repository navigation
Releases: cboxdk/tax-resolver
Release list
v1.0.0 — the API is settled
No code changed from v0.2.0. What changed is the commitment: Authority, JurisdictionAssignment, BoundaryData, Geometry and Resolver are stable surface now, and breaking any of them needs a v2.
That is worth committing to because the shape has been used rather than merely published. cboxdk/laravel-tax resolves US addresses through this package, and the register's own conformance deck — addresses in, expected authority set out, cut from the artifacts a release actually ships — passes through it.
Two readers of one format drifting apart is the failure this package exists to prevent, and it is the failure v0.1.0 shipped: it read a formatVersion 3 artifact as a v2 one, dropped every authority, and answered "no local authority levies here" for every address in all twenty-four Streamlined states. Its own suite stayed green the whole time, because every fixture had been hand-built to match the parser rather than the publisher. A deck that both sides run is the only thing that proves they agree.
What you are committing to as a consumer
- An authority is level AND code. A county and a special district can file under the same number and levy separately, so matching on the bare code merges two bodies that each want their own share.
nullis not[]. Null means no layer answered — fall back to the state rate. An empty list means a row answered "no local authority levies here", which is an answer. Useresolved()rather than checking emptiness.- An unknown
formatVersionthrows. A version this code cannot read has to stop: the alternative is a confident answer that is wrong in the direction nobody audits.
Upgrading from v0.1.0
$assignment->authorities is list<Authority>|null, not list<string>|null. Read $authority->key() ('county:209') or join on {stateFips, $authority->level, $authority->code}.
v0.2.0 — read formatVersion 3, and refuse what it cannot read
Breaking. JurisdictionAssignment::$authorities is now list<Authority>|null, was list<string>|null.
The bug this fixes
An authority set in the boundary format is [{level, code, type?}]. BoundaryData::sets() kept an entry only when it was a bare string — the v2 shape — so against a v3 artifact every entry was dropped and every set came out empty.
An empty set is not nothing in this format. It means a row covers this address and no local authority levies here. So v0.1.0 reported no local tax, with no error, for every address in all twenty-four Streamlined states. Against the real Kansas artifact that is 1029 sets, every one of them wrong.
The suite was green throughout, because every fixture had been hand-built to match the parser's assumption rather than what the register publishes. tests/Fixtures/ks.v3.json is now a genuine slice of boundaries/KS from register release 2026.09.15-202, so that cannot happen the same way twice.
Changed
BoundaryData::resolveZip(),resolveStreet()andGeometry::authoritiesAt()returnAuthorityobjects.BoundaryData::fromArtifacts()andGeometry::fromFeatureCollection()throwUnsupportedFormatVersionfor a version they do not implement, instead of reading what they can. A version this code cannot read has to stop: the alternative is a confident answer that is wrong in the direction nobody audits.
Added
Authority—level,code, optionaltype(the state's own district-type code: 79 and 26 in North Carolina, 63 in Minnesota, 02 in Vermont), andjurisdiction, filled only by the geometry layer, which publishes the register's own code.Authority::key()andis(). Identity is level and code together, never the bare number — a county and a special district can file under the same number and levy separately, so matching on209alone merges two bodies that each want their own share.UnsupportedFormatVersion.
Upgrading
Anywhere you read $assignment->authorities as strings, read $authority->key() ('county:209') or join on {stateFips, $authority->level, $authority->code}. Use $assignment->resolved() rather than checking for an empty array — null and empty mean opposite things.