DOMAIN:EU_REGULATION:ADDRESS_STANDARDS¶
OWNER: aimee (scoping) ALSO_USED_BY: urszula, maxim (backend), floris, floor (frontend), eric (invoicing), ALL dev agents LAST_VERIFIED: 2026-03-26 SCOPE: EU address formats, validation, standardisation, checkout address patterns
Overview¶
Address handling is deceptively complex in multi-country EU e-commerce. Every country has its own postal format, field ordering, validation rules, and postcode structure. Getting this wrong means failed deliveries, returned packages, and frustrated customers.
This page covers the international standards, country-specific EU formats, validation services, implementation patterns, and GDPR considerations for address data.
Cross-reference with Shipping Providers for carrier label format requirements and Peppol E-Invoicing for address fields in structured invoices.
ISO 19160 — International Addressing Standard¶
What It Is¶
ISO 19160 is the multi-part international standard for addressing, jointly developed by ISO and the Universal Postal Union (UPU). It provides a conceptual model for address information that works across all countries and use cases.
Key Parts¶
| Part | Title | Status |
|---|---|---|
| 19160-1 | Conceptual model (address profiles) | Published 2015 |
| 19160-2 | Assigning and maintaining addresses | Published 2023 |
| 19160-3 | Quality of address data | Published 2020 |
| 19160-4 | International postal address components | Published 2017 |
ISO 19160-4 is also published as UPU S42 — they are the same standard.
Address Profile Model¶
ISO 19160-1 defines an address as having three hierarchical levels:
Segments — Functional groups (addressee, delivery point, administrative area)
Constructs — Logical portions (organisation ID, thoroughfare, postal code)
Elements — Atomic values (street name, house number, postal code value)
RELEVANCE_TO_GE: use the segment/construct/element model when designing flexible address schemas. Do not hardcode field names like "address_line_1" — they are meaningless across countries.
UPU S42 — Universal Postal Union Address Standard¶
What It Is¶
UPU S42 (identical to ISO 19160-4) defines 31 address elements that cover every country's postal format. Each country defines a template specifying which elements are used, their order, and their formatting rules.
Core Address Elements¶
The 31 elements include:
| Element | Description | Example (NL) |
|---|---|---|
| Organisation name | Business name | Growing Europe B.V. |
| Building name | Named building | WTC |
| Sub-building indicator | Floor, apartment, room | 4e verdieping |
| Thoroughfare type | Street type (straat, laan, weg) | Straat |
| Thoroughfare name | Street name | Kalverstraat |
| Thoroughfare number | House number | 123 |
| Thoroughfare number suffix | House number addition | A |
| Postcode | Postal code | 1012 PA |
| Town | City / municipality | Amsterdam |
| Province | State / province | Noord-Holland |
| Country | Country name or code | NL |
S42 Templates¶
Each country publishes an S42 template defining which elements are used and how they are rendered. The UPU provides these templates in XML format for machine consumption.
ACCESS: address.post — official S42 template portal with country-specific formats and validation rules.
UPU_API: the UPU Address API provides address capture forms with country-appropriate fields, described in both the local language and Latin script.
EU Country-Specific Address Formats¶
Netherlands (NL)¶
EXAMPLE:
POSTCODE_FORMAT: NNNN LL (4 digits + space + 2 uppercase letters)
POSTCODE_REGEX: ^[1-9]\d{3}\s?[A-Z]{2}$
HOUSE_NUMBER: mandatory, placed after street name
HOUSE_NUMBER_ADDITION: common (A, B, -1, -2, bis, etc.)
UNIQUE_FEATURE: postcode + house number uniquely identifies a Dutch address. No other
fields are strictly needed. This enables postcode lookup — enter postcode + number,
auto-fill street and city.
Germany (DE)¶
EXAMPLE:
POSTCODE_FORMAT: NNNNN (5 digits, leading zero possible)
POSTCODE_REGEX: ^\d{5}$
HOUSE_NUMBER: placed after street name (same as NL)
NOTE: some addresses have no street name (e.g., island of Baltrum)
France (FR)¶
EXAMPLE:
POSTCODE_FORMAT: NNNNN (5 digits, first 2 = department)
POSTCODE_REGEX: ^\d{5}$
HOUSE_NUMBER: placed BEFORE street name (opposite of NL/DE)
CEDEX: organisations receiving large volumes of mail may use CEDEX addresses
Belgium (BE)¶
EXAMPLE:
POSTCODE_FORMAT: NNNN (4 digits)
POSTCODE_REGEX: ^\d{4}$
HOUSE_NUMBER: placed after street name
BIS_NUMBER: optional sub-number (e.g., 215bis)
BOX_NUMBER: apartment/unit identifier (e.g., bus 3)
ABBREVIATION_RULE: abbreviations forbidden in city name unless line exceeds 50 chars
Austria (AT)¶
POSTCODE_FORMAT: NNNN (4 digits)
POSTCODE_REGEX: ^\d{4}$
Spain (ES)¶
POSTCODE_FORMAT: NNNNN (5 digits)
POSTCODE_REGEX: ^\d{5}$
NOTE: floor (piso) and door (puerta) are commonly used
Italy (IT)¶
POSTCODE_FORMAT: NNNNN (5 digits)
PROVINCE_CODE: 2-letter province abbreviation (e.g., MI for Milano)
Address Format Summary¶
| Country | Postcode Format | House # Position | Postcode + HN = Unique | API Available |
|---|---|---|---|---|
| NL | NNNN LL (6) | After street | Yes | Yes |
| DE | NNNNN (5) | After street | No | Limited |
| FR | NNNNN (5) | Before street | No | Limited |
| BE | NNNN (4) | After street | No | Yes |
| AT | NNNN (4) | After street | No | Limited |
| ES | NNNNN (5) | After street | No | Limited |
| IT | NNNNN (5) | After street | No | Limited |
Netherlands-Specific: BAG and Postcode APIs¶
BAG — Basisregistratie Adressen en Gebouwen¶
The BAG is the authoritative Dutch address database, maintained by municipalities and managed nationally by Kadaster. It contains:
- Panden (buildings): geometry, construction year, status
- Verblijfsobjecten (dwellings): usage, floor area
- Nummeraanduidingen (address designations): postcode, house number, addition
- Openbare ruimtes (public spaces): street names
- Woonplaatsen (cities/towns): municipality mapping
DATA_SOURCE: data.overheid.nl/dataset/bag
PDOK_API: https://api.pdok.nl/bzk/locatieserver/search/v3_1/free — free government API
Postcode Lookup Services¶
For Dutch projects, postcode + house number lookup is table stakes. Users expect it.
PostcodeAPI.nu¶
DESCRIPTION: Dutch address data from BAG, built for developers
BASE_URL: https://api.postcodeapi.nu/v3/
AUTH: API key in X-Api-Key header
PRICING: EUR 28.50/year for up to 1,000 calls/month
DATA_SOURCE: Kadaster BAG + CBS open data
DOCUMENTATION: postcodeapi.nu/docs
// PostcodeAPI.nu lookup example
const response = await fetch(
`https://api.postcodeapi.nu/v3/lookup/${postcode}/${houseNumber}`,
{ headers: { 'X-Api-Key': process.env.POSTCODE_API_KEY } }
);
const { street, city, province, municipality } = await response.json();
Postcode.eu (Postcode.nl)¶
DESCRIPTION: official Dutch and international address data via REST
BASE_URL: https://api.postcode.eu/
AUTH: HTTP Basic (API key + secret)
PRICING: EUR 50/year for up to 12,500 calls/year
COUNTRIES: NL, BE, DE, FR, AT, ES, IT, UK, and more
DOCUMENTATION: developer.postcode.eu
BAG_INTEGRATION: supports lookup by BAG Nummeraanduiding ID
RECOMMENDATION_FOR_GE: use Postcode.eu for projects targeting multiple EU countries. Use PostcodeAPI.nu for NL-only projects with higher call volumes.
PDOK BAG API (Free / Government)¶
DESCRIPTION: official government BAG API, free and unlimited
BASE_URL: https://api.pdok.nl/bzk/locatieserver/search/v3_1/
AUTH: none
LIMITATION: raw BAG data, less developer-friendly than commercial APIs
USE_WHEN: budget-constrained projects, internal tools, or bulk geocoding
Address Validation Services¶
EU-Based Options¶
REQUIREMENT: for GDPR compliance, prefer address validation services that process data within the EU. Avoid US-based services that transfer personal address data outside the EEA without adequate safeguards.
| Service | HQ | EU Processing | Countries | API | Pricing |
|---|---|---|---|---|---|
| Postcode.eu | NL | Yes | 15+ EU | REST | From EUR 50/yr |
| PostcodeAPI.nu | NL | Yes | NL only | REST | EUR 28.50/yr |
| HERE Technologies | NL/DE | EU available | Global | REST | Freemium |
| Loqate (GBG) | UK | EU processing | Global | REST | Pay-per-use |
| PDOK | NL (gov) | Yes | NL only | REST | Free |
GE_RECOMMENDATION: Postcode.eu as primary (multi-country, NL-based, BAG-linked). PDOK as fallback for NL-only projects with cost constraints.
Validation Strategy¶
VALIDATE_AT: point of entry (checkout form, address book save) VALIDATE_AGAIN: before shipping label generation (address may have changed) NEVER: validate addresses on every page load — it burns API calls and adds latency
// Address validation flow
async function validateAddress(address: AddressInput): Promise<ValidationResult> {
// 1. Format validation (postcode regex by country)
const formatValid = validatePostcodeFormat(address.country, address.postcode);
if (!formatValid) return { valid: false, error: 'Invalid postcode format' };
// 2. NL-specific: postcode + house number lookup
if (address.country === 'NL') {
const bagResult = await postcodeEuLookup(address.postcode, address.houseNumber);
if (!bagResult) return { valid: false, error: 'Address not found in BAG' };
// Auto-fill street and city from BAG
return { valid: true, enriched: { street: bagResult.street, city: bagResult.city } };
}
// 3. Other countries: format check + optional API validation
return { valid: true };
}
Checkout Address Patterns¶
Shipping vs Billing Address¶
DEFAULT: pre-check "billing address same as shipping" — 85%+ of orders use same address. Show billing address fields only when unchecked.
// Address form state
interface CheckoutAddressState {
shippingAddress: Address;
billingAddressSameAsShipping: boolean; // default: true
billingAddress?: Address; // only when different
}
Address Book (Authenticated Users)¶
PATTERN: users save multiple addresses with labels (Home, Work, Partner). One default shipping address, one default billing address.
CREATE TABLE user_addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
label TEXT, -- 'Home', 'Work', etc.
is_default_shipping BOOLEAN DEFAULT false,
is_default_billing BOOLEAN DEFAULT false,
-- Address fields (see schema below)
country_code TEXT NOT NULL,
postcode TEXT NOT NULL,
city TEXT NOT NULL,
street TEXT,
house_number TEXT,
house_number_addition TEXT,
address_line_1 TEXT, -- for countries without structured fields
address_line_2 TEXT,
province TEXT,
company_name TEXT,
-- Metadata
validated_at TIMESTAMPTZ, -- last validation timestamp
bag_id TEXT, -- NL: BAG nummeraanduiding ID
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Only one default per type per user
CREATE UNIQUE INDEX idx_default_shipping ON user_addresses(user_id) WHERE is_default_shipping;
CREATE UNIQUE INDEX idx_default_billing ON user_addresses(user_id) WHERE is_default_billing;
Guest Checkout Address¶
Guest checkout must collect address without requiring account creation. Store address with the order, not in a user address book.
GDPR_RULE: guest checkout addresses are retained only as long as needed for order fulfilment and legal retention periods (typically 7 years for tax/invoice records in NL). After that, anonymise or delete.
Country-Specific Form Fields¶
The checkout address form must adapt its fields based on the selected country:
// Country-specific field configuration
const addressFieldConfig: Record<string, AddressFieldConfig> = {
NL: {
fields: ['postcode', 'houseNumber', 'houseNumberAddition'],
autoFill: ['street', 'city'], // from postcode API
postcodeLabel: 'Postcode',
postcodeFormat: 'NNNN LL',
houseNumberRequired: true,
},
DE: {
fields: ['street', 'houseNumber', 'postcode', 'city'],
autoFill: [],
postcodeLabel: 'PLZ',
postcodeFormat: 'NNNNN',
houseNumberRequired: true,
},
FR: {
fields: ['houseNumber', 'street', 'postcode', 'city'], // number BEFORE street
autoFill: [],
postcodeLabel: 'Code postal',
postcodeFormat: 'NNNNN',
houseNumberRequired: true,
},
BE: {
fields: ['street', 'houseNumber', 'busNumber', 'postcode', 'city'],
autoFill: [],
postcodeLabel: 'Postcode',
postcodeFormat: 'NNNN',
houseNumberRequired: true,
},
};
GDPR Considerations¶
Address as Personal Data¶
Addresses are personal data under GDPR. A postal address combined with a name directly identifies a natural person.
OBLIGATIONS: - Lawful basis for processing (contract performance for order fulfilment) - Data minimisation — only collect fields needed for delivery - Retention limitation — delete or anonymise when no longer needed - Right to erasure — must be able to delete address data on request (except where legal retention periods apply, e.g., invoice records) - Storage security — encrypt at rest, restrict access
Retention Rules¶
| Data Type | Retention (NL) | Legal Basis |
|---|---|---|
| Shipping address | Duration of order fulfilment + returns | Contract performance |
| Billing address | 7 years (fiscale bewaarplicht) | Legal obligation (AWR) |
| Address book | Until user deletes or account is closed | Consent / contract |
| Guest checkout | Order fulfilment + legal retention | Contract + legal obligation |
Shipping Label Format Requirements¶
Each carrier has specific address formatting requirements for labels. See Shipping Providers for details.
GENERAL_RULES: - Country name in local language OR ISO 3166-1 alpha-2 code - City in UPPERCASE for international shipments - Postcode before or after city depending on country convention - Maximum line length: typically 35-40 characters per line - No special characters that the carrier's system cannot render
GE Drizzle Schema — Flexible EU Address¶
// Drizzle schema pattern for multi-country EU addresses
export const addresses = pgTable('addresses', {
id: uuid('id').defaultRandom().primaryKey(),
// Core fields (all countries)
countryCode: text('country_code').notNull(), // ISO 3166-1 alpha-2
postcode: text('postcode').notNull(),
city: text('city').notNull(),
// Structured fields (NL, DE, BE, AT, etc.)
street: text('street'),
houseNumber: text('house_number'),
houseNumberAddition: text('house_number_addition'), // NL: toevoeging
busNumber: text('bus_number'), // BE: bus number
// Unstructured fallback (FR, ES, IT, international)
addressLine1: text('address_line_1'),
addressLine2: text('address_line_2'),
// Administrative
province: text('province'), // optional
companyName: text('company_name'),
// Validation metadata
validatedAt: timestamp('validated_at'),
validationSource: text('validation_source'), // 'bag', 'postcode_eu', etc.
bagId: text('bag_id'), // NL: nummeraanduiding ID
// Audit
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
DESIGN_DECISION: use both structured fields (street, house number) AND unstructured fallback (address_line_1/2). For NL and DE, populate structured fields from postcode lookup. For countries without reliable postcode APIs, use address_line_1/2.
NEVER: store the formatted/rendered address as a single text field. Always store atomic components. Rendering is a presentation concern — the format changes by context (label, invoice, display).
Sources¶
- ISO 19160-4 / UPU S42 Addressing Standard
- UPU S42 International Addressing Standards
- UPU Addressing Solutions
- BAG — Basisregistratie Adressen en Gebouwen
- PostcodeAPI.nu Documentation
- Postcode.eu Developer Docs
- PDOK BAG Locatieserver
- Address Formats by Country (Geoapify)
- W3C International Address Formats
- Address Format by Country (Wikipedia)