Address form field order by country

A Japanese customer’s delivery address arrives with the prefecture in the “State” field, the building name appended to the street line, and the postcode in a box that rejected it twice for having a hyphen. A German customer is asked for a state that does not exist in their address. A British customer’s postcode fails a five-digit validation.

The form was designed once, against one country’s conventions, and every other country is being asked to fit into it.

The same address slots across four countries The first administrative level is a state in the United States, a county in the United Kingdom, absent in Germany and a prefecture in Japan. The second level is a city, town, city and city or ward respectively. Postcodes are five digits, alphanumeric, five digits, and seven digits with a hyphen. Only two of the four countries require the first level at all. One field, four meanings US GB DE JP address-level1 State County Prefecture address-level2 City Town City City / ward postal-code ZIP, 5 digits Postcode, alphanumeric PLZ, 5 digits 7 digits with a hyphen Required? state yes county no no region prefecture yes
Four columns, no shared row. A single hardcoded form is one of these four and wrong for the rest.

Root cause: an address is a country’s data structure, not a universal one

There is no international address schema. What exists is a set of national conventions that differ in which components exist, what they are called, what order they appear in, which are mandatory, and how each is formatted.

Three differences account for most failures.

Component presence. A first-level administrative division is essential in the United States, Japan, Brazil and Australia; meaningless in Germany, the Netherlands and most of Europe. Rendering it always produces a required field a German customer cannot fill; omitting it always produces an address a Japanese courier cannot use.

Naming. The same slot is a state, a province, a county, a prefecture or a region. Translating the word “State” into German does not help, because the problem is not the language — it is that the concept does not apply.

Order. Most of the world writes an address smallest unit first; Japan, China and Korea write it largest first. That is a reversal rather than a rearrangement, which is why a fixed field order cannot be made to work for both by relabelling.

Address ordering conventions Most countries order an address from the smallest unit to the largest, beginning with the recipient and street and ending with the country. Japan, China and Korea order from the largest unit to the smallest, beginning with the postcode and prefecture and ending with the recipient. Two orders, both correct Smallest first — most of the world · Recipient · Street and number · Postcode and city · Country Largest first — Japan, China, Korea · Postcode · Prefecture · City / ward · District and block · Recipient These are not variations of one layout: the sequence is reversed, so a single field order cannot serve both.
A form that renders the left column for a Japanese address produces something a postal worker has to reassemble.

Minimal reproducible example

// One form, one country's assumptions
<input name="street"   placeholder="Street address" />
<input name="city"     placeholder="City" />
<select name="state" required>{US_STATES.map(...)}</select>
<input name="zip"      pattern="\d{5}" required placeholder="ZIP code" />

Every line is a decision that is correct in one country. The pattern rejects the United Kingdom, Canada and the Netherlands; the required state blocks most of Europe; the order is wrong for Japan.

The fix: a schema per country, and the country first

type AddressField = {
  name: 'street' | 'street2' | 'city' | 'region' | 'postalCode';
  labelKey: string;          // translated, e.g. 'address.region.prefecture'
  required: boolean;
  pattern?: RegExp;
  autocomplete: string;      // the standard token, not a country word
};

export const ADDRESS_SCHEMAS: Record<string, AddressField[]> = {
  US: [
    { name: 'street', labelKey: 'address.street', required: true, autocomplete: 'address-line1' },
    { name: 'city', labelKey: 'address.city', required: true, autocomplete: 'address-level2' },
    { name: 'region', labelKey: 'address.region.state', required: true, autocomplete: 'address-level1' },
    { name: 'postalCode', labelKey: 'address.postal.zip', required: true,
      pattern: /^\d{5}(-\d{4})?$/, autocomplete: 'postal-code' },
  ],
  DE: [
    { name: 'street', labelKey: 'address.street', required: true, autocomplete: 'address-line1' },
    { name: 'postalCode', labelKey: 'address.postal.plz', required: true,
      pattern: /^\d{5}$/, autocomplete: 'postal-code' },
    { name: 'city', labelKey: 'address.city', required: true, autocomplete: 'address-level2' },
  ],
  JP: [
    { name: 'postalCode', labelKey: 'address.postal.jp', required: true,
      pattern: /^\d{3}-?\d{4}$/, autocomplete: 'postal-code' },
    { name: 'region', labelKey: 'address.region.prefecture', required: true, autocomplete: 'address-level1' },
    { name: 'city', labelKey: 'address.city', required: true, autocomplete: 'address-level2' },
    { name: 'street', labelKey: 'address.street.jp', required: true, autocomplete: 'address-line1' },
  ],
};

export const GENERIC: AddressField[] = [
  { name: 'street', labelKey: 'address.street', required: true, autocomplete: 'address-line1' },
  { name: 'street2', labelKey: 'address.street2', required: false, autocomplete: 'address-line2' },
  { name: 'city', labelKey: 'address.city', required: true, autocomplete: 'address-level2' },
  { name: 'region', labelKey: 'address.region.generic', required: false, autocomplete: 'address-level1' },
  { name: 'postalCode', labelKey: 'address.postal.generic', required: false, autocomplete: 'postal-code' },
];

The schema carries the order by being an array, which is what makes Japan expressible without a special case in the component. The label is a translation key rather than a word, so “Prefecture” is translated into every interface language while remaining the correct concept for Japan.

Five steps for a data-driven address form The country selector sits above the address fields and re-renders them when it changes. A schema for that country supplies the field list, their order, their label keys, which are required and how each validates. Fields render in the schema order rather than a fixed component order. Labels come from translated keys named in the schema. And an uncurated country falls back to a generic international form rather than to one country conventions. Rendering an address form from data 1 Country selector, above everything changing it re-renders the rest 2 Look up the schema for that country fields, order, labels, required, pattern 3 Render in the schema order not in a fixed component order 4 Label from the schema, translate the label key "county" is a key, not a string 5 Fall back to a generic international form for countries you have not curated
The last step is what makes the other four affordable: you curate the countries you ship to, not all of them.

The generic fallback is the part that scales

Curating schemas for every territory is a project without an end. Curating the ten or twenty you actually ship to, and falling back to a generic international form everywhere else, gets nearly all of the benefit.

The generic form must genuinely be generic: street lines, a city, an optional region with a neutral label, an optional postcode with no pattern. What it must not be is one country’s form used as a default, which is how an American form ends up in front of a customer in a country nobody thought about.

Two rules keep the fallback honest. Nothing beyond street and city is required, because in some territory each of the other fields is absent. And no pattern validation runs, because a pattern that fits nothing in particular rejects real addresses. A generic form accepts more than it should, which is the correct failure direction for an address: a courier can interpret an oddly formatted address, and cannot deliver one that was never submitted.

Formatting the address back out

Collecting an address correctly is half the job; rendering it is the other half, and it uses the same per-country data in the opposite direction.

An address appears in several places after collection — an order confirmation, a shipping label, an invoice, a customer record — and each of them needs the components assembled into lines according to the destination country’s convention rather than the reader’s. A German address printed in American order is as unusable to a German courier as it would be collected that way.

The assembly rule is a per-country template over the same component names, which means the schema that drives the form can drive the rendering too:

const LABEL_TEMPLATES: Record<string, string[]> = {
  US: ['{recipient}', '{street}', '{city}, {region} {postalCode}', '{country}'],
  DE: ['{recipient}', '{street}', '{postalCode} {city}', '{country}'],
  JP: ['{postalCode}', '{region}{city}', '{street}', '{recipient}'],
};

Two details matter in practice. The country line is omitted for a domestic address and required for an international one, so the template needs to know where the parcel is going from as well as to. And the label should be in the destination’s own language and script where possible, because the person reading it is a postal worker in that country — an address for Japan is best printed in Japanese, with a Latin transliteration underneath rather than instead.

That last point is the one that surprises teams whose interface is fully localized: the shipping label is the one surface where the reader’s locale is irrelevant.

Verification

test.each([
  ['US', ['street', 'city', 'region', 'postalCode']],
  ['DE', ['street', 'postalCode', 'city']],
  ['JP', ['postalCode', 'region', 'city', 'street']],
])('%s renders fields in the local order', (country, expected) => {
  const { getAllByRole } = render(<AddressForm country={country} />);
  expect(getAllByRole('textbox').map((i) => i.getAttribute('name'))).toEqual(expected);
});

test.each([
  ['US', '94103', true], ['US', 'SW1A 1AA', false],
  ['GB', 'SW1A 1AA', true], ['GB', '94103', false],
  ['NL', '1012 AB', true],
])('%s postcode %s valid=%s', (country, code, ok) => {
  expect(isValidPostalCode(country, code)).toBe(ok);
});

The Dutch row is worth keeping specifically: 1012 AB contains a space and letters, and it is the value that most often reveals a validation rule written against digits.

When to escalate

If addresses validate and still fail delivery, the problem may be the join rather than the fields. An address stored as separate components has to be assembled into lines for a label, and the assembly order is per country too — the same schema can drive it.

If a courier or payment provider rejects an address your form accepted, compare their required fields against the schema. Providers publish per-country requirements, and their rules are the operative ones for anything that ships.

If a customer insists their address does not fit any field, believe them. Rural addresses, addresses without street names, and PO-box-only territories all exist, and a free-text line that is passed through untouched is the pressure valve that keeps them able to buy.

FAQ

Should the country be a selector or derived from the locale?

A selector, defaulting to a guess. Locale and country are different questions — an American living in Berlin has an English interface and a German address — and deriving one from the other guarantees a wrong default for exactly the people most likely to notice.

Where do the schemas come from?

Published address-format data derived from postal authorities covers most territories, and payment providers expose per-country requirements through their APIs. For the countries you ship to most, curating by hand from either source is a day of work and stays correct for years.

Do I need to validate postcodes at all?

Lightly. A pattern per country catches transposed digits and pasted junk, which is worth having. What is not worth having is a strict pattern that rejects a valid but unusual code — a warning that lets the customer proceed is a better shape than a block.

How should the address be stored?

As components plus the country code, not as a single formatted string. Components can be re-rendered in any convention, sent to any provider, and corrected field by field; a formatted blob can only be reparsed by guessing.

Part of Internationalizing Forms & Input.