An address is checked against the rules of its own country #147
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Depends on
Reference
Postulo/postulo#147
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Observation
Extends #92, which specifies the model and already settles not unique across the instance
— unique per owner, freely shared between accounts, because two people at one address is a
household and not a mistake. What this adds is the country-by-country rules and where they
live.
What exists
No postal address anywhere.
Profile.locationis a 120-character line whose help text sayswhat it is for — "City and country, as it should appear on a CV" — a display string, not an
address, and right to be one.
#92 proposes the parts every format agrees on — address line, postcode, municipality, region,
country code — and already says formats differ:
It also fixes what valid may mean, and the reasoning is inherited from
phones.py:A country list already exists and should not be built twice.
phones.COUNTRIESholds ISO3166-1 alpha-2 with English names,
assets/flags.txtand the{% flag %}tag draw them, andphones.default_country()guesses from the person's language. A country chooser is a solvedproblem here.
What this asks for
Per-country rules — which fields are required, what a postcode looks like, what each field is
called, and what order they print in — held by the plugin rather than by the application.
Worth being careful about
Naming the fields is a harder problem than validating them, and it is not a translation
problem. A person reading Postulo in Portuguese who enters a United States address should
see State; entering a Portuguese one, Distrito; a Japanese one, Prefecture. The label
depends on the address's country, not on the reader's language, and both vary at once.
Nothing in this codebase has that shape yet: every string so far is chosen by the reader's
language alone. Whatever data set is used has to carry the label keys, and the interface has
to be able to say them in the reader's language.
Several countries have no postcode at all, and several acquired one recently — Ireland's
Eircode arrived in 2015. A required-field rule that assumes a postcode exists produces a form
somebody cannot complete for the place they actually live.
Refusing to save is the failure mode to design against.
phones.pykeeps an unparseablenumber exactly as typed, and the same must hold here: BFPO addresses, rural routes, informal
settlements, temporary accommodation, and simply a data set that is wrong about somewhere.
The rules warn; they never prevent somebody recording where they live. That is a matter
of dignity as much as correctness — an application that will not accept your address is
telling you something about who it was written for.
Where the rules come from, and the one that is ruled out.
Google's
libaddressinputis the set everybody reaches for — required fields, validation andform layout per country, the metadata Chromium's autofill uses — and it is not the answer
here. The maintainer's position is that Postulo should not depend on Google code, and it is
the right call for a project whose 0.3.0 notes say it exists as an alternative to services
answering to somebody else's jurisdiction. Recording it so the next person does not propose
it again: the data is also served from a Google endpoint, which Postulo could not call at
render time anyway.
The problem splits in two, and only one half has a ready answer.
Layout is solved, cleanly and elsewhere. OpenCage's
address-formatting is MIT, covers 251
territories with both templates and test cases, and does exactly one thing: renders the
parts of an address in the right order for its country. It explicitly does not validate,
and postcode-format validation is on its roadmap rather than in it. For the half of this
issue that is "where does the postcode go, is a region named at all", it is complete, it is
data rather than code, and it carries no dependency worth the name.
Required fields, postcode shapes and field labels have no obvious non-Google source, and
the honest options are worse than "pick a library":
UPU's data products are commercial. Individual operators publish their own formats, in
their own words, in their own places.
vary per file and per country.
P281is the postcode of a place, not the format for a country — whatever format dataexists there needs its coverage checked before it is relied on.
Which points at the answer this codebase already uses everywhere else: a curated table.
phones.COUNTRIESis 250-odd hand-written rows with a comment explaining why the names arein English.
NATIVE_NAMES,PLURAL_FORMSandFLAG_COUNTRIESare the same shape, eachentry decided rather than derived, several carrying a note about the trap in that particular
language. An address-rules table maintained the same way needs no third-party licence, no
vendored snapshot and no update pipeline, and it can start with the countries whose languages
Postulo already speaks and grow the way the languages did — deliberately, in order, with the
reasoning per entry.
It also fails in the right direction. A country with no rules yet gets no rules, which means
free-form entry and no refusal — which is what this issue requires anyway.
Whatever is vendored records when it was taken, exactly as #140 concludes for NACE: a
data file with its version, not a dict in a module.
"Inside the plugin" means the plugin ships a data file, which makes it the largest thing
the plugin owns — larger than its models. That runs into #128 (what happens to a plugin's
data when the plugin goes), #127 (a built-in plugin's own catalogues, and here the label
strings are the awkward ones) and #129.
Postal is the channel that proves the contract in #146 needs a third state. It can be
validated and it cannot be confirmed, short of posting something to it — which is what some
services do and this project will not. Validated-but-unconfirmable has to be a legitimate
answer, and this is the issue that makes it concrete.
#92 already routes the privacy question correctly — a home address belongs in
docs/THREAT-MODEL.md, andProfile.locationkeeps printing a city and country on a CV andnever a street. None of that changes here; it is worth not undoing by accident when the
rendering moves into a plugin.
Corrected: the Google data set is out. The first version of this issue recommended
libaddressinput, which sits badly with a project whose own release notes give European digital sovereignty as the reason for its language order. The body now says so and says why, so it does not get proposed again.What the research changed, rather than just removing an option:
address-formatting, MIT, 251 territories, templates and test cases, complete since March 2024. It renders and does not validate, which is exactly half of what this issue needs.P281is a place's postcode rather than a country's format.phones.COUNTRIES,PLURAL_FORMS,NATIVE_NAMESandFLAG_COUNTRIES— no licence, no snapshot, no pipeline, and it can start with the countries whose languages Postulo already speaks. A country with no rules yet gets free-form entry, which is the required behaviour regardless.Both halves, and the harder one was not validation.
Naming the fields
So the table names a key and the catalogue supplies the word. A Portuguese reader
entering a United States address sees Estado; the same reader entering a Portuguese one
sees Distrito; a Japanese one, Prefeitura. Sixteen label keys across region, postcode and
town — and the ones that are names rather than descriptions stay names: an Eircode is an
Eircode in Lisbon, and a CAP is a CAP in Dublin.
Where the rules come from
A curated table, which is what the issue's own analysis points at: the shape
phones.COUNTRIES,NATIVE_NAMESandPLURAL_FORMSalready use — rows decided rather thanderived, reasoning per entry, no third-party licence, no vendored snapshot, no update
pipeline. 36 countries: those whose languages Postulo speaks, plus the ones its people most
often apply to. It records when it was reviewed, as #140 concludes for NACE.
libaddressinputis ruled out in the module itself, so the next person does not proposeit again — a project that exists as an alternative to services answering to somebody else's
jurisdiction does not put that jurisdiction in its address form, and a test asserts the
reasoning is still written down.
OpenCage's
address-formattingis credited and not vendored. Theordercolumn is theiridea, hand-written; the file stays theirs, and the module says to read it before extending
this.
Every warning
No postcode at all, and postcodes that arrived recently. Ireland expects no Eircode —
it arrived in 2015 and plenty of addresses predate it — and that is a test rather than a
comment.
Refusing to save is the failure mode designed against. Nothing here refuses. Every rule
is a note beside the row, computed in
_post_cleanand never throughadd_error. An addressreading
BFPO 123with a postcode of not a postcode at all saves, keeps every character,and says what Portugal usually expects.
phones.py's rule for an unparseable number, appliedto somewhere somebody lives.
A country with no row gets no rules, which is also byte for byte what switching the
feature off does — the same code path rather than a second one nobody has seen, and tested as
such.
#92's privacy routing is not undone.
Profile.locationstill prints a town and a countryand never a street; the rendering that moved into the plugin is for a letter, and the CV
header does not use it.
What is deferred, and named
The issue lists the collisions this runs into — #128 on a plugin's data, #146 on a channel
that can be validated and never confirmed. The rules table is code in the plugin's package
rather than a data file it owns, so #128 has nothing new to answer here; and postal as a
confirmable channel is #146's to settle, not this one's — nothing here claims an address is
confirmed, because nothing could.
21 tests, 22 strings in all 39 European catalogues. Shipped in
fd9c6ffon0.3.0, withmainkept level.