Names, addresses and countries
On this page
These types hold people's names, postal addresses and email addresses, and the closed lists every app needs: countries and their subdivisions, languages, scripts, continents, nationalities. Each list is a built-in enum, read by its code and shown by its name in the reader's language.
Here a shop sends parcels abroad and prints each label for the person who packs it, greets its customers by name, and reads the countries a supplier's file gives by their three-letter codes:
type FileText = Text {}type UnknownCountry = Failure { kind: FailureKind.invalid_value}/// A parcel's label, as its packer reads an address: in the order of the/// parcel's country for the packer's language.function parcel_label(parcel_to: PostalAddress, packer: Locale) -> PresentationText { return text.format(parcel_to, locale: packer)}/// A customer's name as a greeting writes it in the reader's language.function greeting_name(customer: PersonName, viewer: Locale) -> PresentationText { return text.format(customer, locale: viewer)}/// The country a supplier's file gives by its three-letter code.function supplier_country(raw: FileText) fails UnknownCountry -> Country { var made_in: Country = attempt text.parse(raw, as: Country, form: CodeForm.alpha3) { malformed: UnknownCountry } return made_in}
test "an address is in its country's order for the reader" { var jp_parcel: PostalAddress = PostalAddress { lines: ["丸の内2-7-2"] locality: "千代田区" region: "東京都" postal_code: "100-8994" country: Country.jp } assert parcel_label(jp_parcel, Locale.ja_jp) == "〒100-8994\n東京都千代田区\n丸の内2-7-2" assert parcel_label(jp_parcel, Locale.en_gb) == "丸の内2-7-2\n千代田区, 東京都\n100-8994\nJapan" var de_parcel: PostalAddress = PostalAddress { lines: ["Platz der Republik 1"] locality: "Berlin" region: none postal_code: "11011" country: Country.de } assert parcel_label(de_parcel, Locale.de_de) == "Platz der Republik 1\n11011 Berlin" assert parcel_label(de_parcel, Locale.en_us) == "Platz der Republik 1\n11011 Berlin\nGermany"}test "a name is in the order the reader's language gives" { var countess: PersonName = PersonName { given: "Ada" family: "Lovelace" } assert greeting_name(countess, Locale.en_gb) == "Ada Lovelace" assert greeting_name(countess, Locale.hu_hu) == "Lovelace Ada" var yamada: PersonName = PersonName { given: "太郎" family: "山田" } assert greeting_name(yamada, Locale.en_gb) == "山田太郎"}test "a closed list reads its codes and its other code systems" { var made_in: Country = attempt supplier_country("gbr") assert made_in == Country.gb assert supplier_country("UK") fails UnknownCountry assert text.from(Country.in_) == "IN" assert text.from(Country.gb, form: CodeForm.numeric) == "826" assert text.format(Country.gb, locale: Locale.fr_fr) == "Royaume-Uni" assert text.format(Language.ja, locale: Locale.en_gb) == "Japanese" assert list.count(Country.all) == 249}
A name and an address are stored as a column per part, so a query can read a town or a country as it reads any column. They come back as they were written:
/// Someone the mailroom sends parcels to.entity Recipient { id: Uuid name: PersonName // columns `name_given` and `name_family` ship_to: PostalAddress // a column per part, its lines a `text[]` citizen_of: Nationality? // its code, `IN`}
use mailroom/entities { Recipient }use mailroom/actions { readdress }test "a name and an address are kept part by part" { var enrolled: Recipient = enrol( PersonName { given: "Mary" family: none }, PostalAddress { lines: ["Flat 2", "14 Rue de Rivoli"] locality: "Paris" region: none postal_code: "75004" country: Country.fr }, Nationality.in_ ) var kept_record: Recipient = attempt Recipient.get(enrolled.id) assert kept_record.name == enrolled.name assert kept_record.ship_to == enrolled.ship_to assert kept_record.citizen_of == Nationality.in_ var readdressed: Recipient = readdress( kept_record, PostalAddress { lines: ["2-7-2 Marunouchi"] locality: "Chiyoda-ku" region: "Tokyo" postal_code: "100-8994" country: Country.jp } ) var kept_after: Recipient = attempt Recipient.get(enrolled.id) assert kept_after.ship_to == readdressed.ship_to}
The lists, and where they come from
Each list is shipped with the release, generated once into
jadpo_std::standard::places_data:
Countryis ISO 3166-1's 249 assigned codes as CLDR 48.2 carries them: its regular regions, less the exceptionally reservedAC,CP,CQ,DG,EA,ICandTAand the user-assignedXK. Its alpha-3 and numeric codes come from CLDR's code mappings. Its retired codes are the eleven ISO 3166-3 codes CLDR marks deprecated (AN,BU,CS,DD,FX,NT,SU,TP,YD,YU,ZR), shown by ISO 3166-3's English names, because CLDR names none of them.CountrySubdivisionis ISO 3166-2 as CLDR 48.2 carries it: 5,027 codes, with the 626 that CLDR marks deprecated retired.Languageis ISO 639-1's 183 codes, from the Library of Congress's ISO 639-2 list (2026-10-18). Its alpha-3 form is ISO 639-2/T.in,iw,ji,jwandmoare retired. A language with only a three-letter code isn't in the list.WritingScriptis ISO 15924's 224 codes (2026-10-18), less the private-use range. Its numeric form is ISO 15924's number.Continentis UN M49's five regions, and Antarctica, which M49 lists beside them. It follows the authority's list, so the Americas are one region. Its codes are digits, so each variant's code is its own name, as aWeekday's is, and M49's number is its numeric form.TextDirectionis the enumlocale.directiongives. Its codes areltrandrtl, as HTML'sdirattribute writes them.NationalitytakesCountry's codes and names, as a passport names a nationality by its country. CLDR has no names for nationalities, and ICAO's codes for travel documents belong toIdentityDocument.Currencyalso holds the six codes ISO 4217 has withdrawn since 2021 (ANG,BGN,CUC,HRK,SLL,ZWL), retired.
Closed lists are enums
A standard type is a built-in enum when its list is closed, published by
an authority and rarely changes, as Locale, Zone and Weekday are. A
huge or fast-changing list stays text with a pattern:, and is checked
against its list where this page says so: IataAirport, PostalCode,
HsCode, UnLocode, Icd10Code, SpdxLicence, Emoji.
HttpStatusstays a number, because its registry is open by design: an unknown code is still a status.CallingCodestays text, because its codes are digits.CountrySubdivision, about 5,000 codes with a few changes a year, is an enum. Validating an address needs the list anyway, CLDR names each subdivision, and its changes arrive with releases, as the time-zone database's do.
Jadpo doesn't match names such as "England" or "UK" to codes, in
text.parse or in any operation. Which name means which code is a
judgement, and it differs by locale: England is
CountrySubdivision.gb_eng, not a country. A dropdown built from .all
already asks a person to pick. A package can offer name matching if an
import needs it.
Variants and .all
A variant is its code in lower case with _ for -:
CountrySubdivision.gb_eng, as Locale writes its variants. A code that
is a reserved word, or is
all, takes a trailing _: Country.in_, Language.or_.
.all lists an enum's variants in its order, a closed list's by
code, as a List of the enum. Retired codes are left out, because a
dropdown never offers Yugoslavia. On an enum whose variants carry data,
.all is type.unknown_enum_variant, and a variant called all is
type.reserved_variant_name.
Retired codes
A retired code is refused where code writes it as a value, comparisons
included: type.variant_retired. You can decline it, and the decline's
reason says why the code still needs the old value. A match arm on a
retired code isn't refused, because it only reads, and a match lists
every variant anyway (Match).
Reading codes: form:
form: picks which of a list's code systems to read or write, as a
CodeForm written out: CodeForm.alpha3, CodeForm.numeric. A value
held in a variable is type.mismatch. A form the type has no codes in is
type.temporal_option_unused, as an option that means nothing for a type
is.
text.parse reads a form's code ignoring case. Where a retired variant
shares a code with a current one, the code reads as the current one:
ind is Language.id, not Language.in_.
At a boundary, such as a request, JSON, a CSV cell or a setting, a
standard enum's code is read ignoring case and written back in its
canonical case. No two codes in these lists differ only by case, so that
is safe. An unknown code is invalid_value naming the field, with the
rule variant. Spaces and other spellings are still refused.
Stored and shown
Stored: a closed list's column holds its code, as Currency's does:
GB, and IN rather than in_. An object is stored as a column per
part, named <field>_<part>. A part that may be none is a column that
may be NULL. A PostalAddress's lines are a text[] column, and its
country is its code.
Shown: a country, a continent, a language and a script by CLDR's name in the reader's locale, through ICU4X's display names, and a withdrawn code by its list's English name. A subdivision is shown by CLDR's English name in every locale, because CLDR names few in other languages and ICU4X doesn't include them. A continent is shown by CLDR's name for its M49 region, and Antarctica by its territory's name.
People's names
PersonName is a given name and, where the person has one, a family
name. A single name is a given name, as CLDR holds a mononym, so no rule
across the fields is needed. It has no single text form, and its JSON is
its parts.
text.format applies CLDR's medium, formal pattern for referring to a
person, from CLDR 48.2's person-name data:
- the name's locale is the reader's when the name is in the reader's script; otherwise it is the likeliest locale for the name's script, which then formats it;
- the reader's locale's lists give the order of the parts;
- the name's locale gives the space between them, none in Japanese.
Jadpo applies the patterns itself. ICU4X's formatter is experimental, leaves a pattern's literals behind ("Ada Lovelace ,") and doesn't work out the order.
Addresses
PostalAddress is its lines as written, then a locality, a region
and a postal code, each one line that may be none, and its Country.
The region is text as the envelope writes it, not a
CountrySubdivision. A dependent locality or a sorting code goes in the
lines, and the recipient's name is your application's own field.
CLDR has no address formats, so text.format uses libaddressinput's,
Google's formats from UPU data (Apache-2.0, commit 81eb962 of
2026-05-15):
- a reader of one of the country's languages gets the country's own format, and anyone else its Latin-script format;
- the text between two fields appears only where the fields beside it are there;
- the country's CLDR name comes last for a reader in another country.
Nothing is put in capitals. UPU's capitals are for the envelope, not for people reading.
EmailWithName is a name that may be none and an Email. Its
canonical text is RFC 5322's mailbox with RFC 6532's UTF-8:
Ada Lovelace <ada@example.com>. The name is quoted only where it must
be, and an address with no name is written alone. Only that text is
read. Its JSON is its parts.
Lines and other text
A line, an object's part of text, is 1 to 200 characters with no control character and no space at either end, as an invoice number is. A literal is checked while compiling.
These are held as text:
GivenNameandFamilyNameare a line;Usernameis[a-z0-9_.-]{1,64};PhoneExtensionis up to 20 digits, the longest libphonenumber reads afterext.;Uprn, a UK property's unique reference number, is 1 to 12 digits with no leading zero, held as text so that no digit is lost.
Personal data and shapes
Personal data: a name, a given or family name and a username are
identifying; a mailbox, an address, an extension and a UPRN are
contact; a nationality is demographic. The lists of places aren't
personal data.
No shapes: type.standard_exists matches none of the objects,
because { name, email } and { first_name, last_name } are too common
in applications' own types to be a sign that a type is one.
Markdown for agents: /docs/names-addresses-and-countries.md.Built from Jadpo main at a707efb.