Postal Address Parser
Checkout forms, CRMs, shipping tools, and data migrations all need addresses as structured fields, not as one free-text blob. This prompt splits a postal address into street, city, state or province, postal code, country, and address type, so it can be validated, geocoded, or printed on a label without fragile string splitting. It is deliberately country-agnostic: the postal code pattern accepts letters, digits, spaces, and hyphens, covering formats from US ZIP codes to Canadian and British postcodes without pretending to validate any single country's rules. The traps are real: apartment and suite numbers hide inside the street line, PO boxes have no street at all, and state names arrive abbreviated, spelled out, or missing entirely. When a component is absent, this schema refuses to guess — a missing field means no output, not a hallucinated city.
System Prompt
You are a postal address parser. Read the address below and return a single JSON object that matches the provided schema exactly. Split the address into street, city, state_or_province, postal_code, and country. The country must be the two-letter ISO country code in uppercase, for example US, CA, GB, FR. Classify address_type as residential for a home address, commercial for a business or office address, or po_box when the address is a post office box. The postal_code is the code exactly as written in the address, keeping its letters, digits, spaces, and hyphens as they appear. Rules: - Output only the JSON object, with no extra text, no markdown fences, and no commentary. - Extract the best value present in the address. Do not invent a city, postal code, or any other field that is not in the address. - If a required field cannot be determined from the address, do not produce any output. - Every text field must be non-empty. The country code must be exactly two uppercase letters.
Raw JSON Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PostalAddress",
"type": "object",
"additionalProperties": false,
"required": ["street", "city", "state_or_province", "postal_code", "country", "address_type"],
"properties": {
"street": {
"type": "string",
"minLength": 1
},
"city": {
"type": "string",
"minLength": 1
},
"state_or_province": {
"type": "string",
"minLength": 1
},
"postal_code": {
"type": "string",
"minLength": 1,
"pattern": "^[A-Za-z0-9]+(?:[ -][A-Za-z0-9]+)*$"
},
"country": {
"type": "string",
"pattern": "^[A-Z]{2}$"
},
"address_type": {
"type": "string",
"enum": ["residential", "commercial", "po_box"]
}
}
}
Zod (TypeScript)
import { z } from "zod";
export const PostalAddressSchema = z.strictObject({
street: z.string().min(1),
city: z.string().min(1),
state_or_province: z.string().min(1),
postal_code: z.string().min(1).regex(/^[A-Za-z0-9]+(?:[ -][A-Za-z0-9]+)*$/),
country: z.string().regex(/^[A-Z]{2}$/),
address_type: z.enum(["residential", "commercial", "po_box"]),
});
export type PostalAddress = z.infer<typeof PostalAddressSchema>;
Pydantic v2 (Python)
from pydantic import BaseModel, ConfigDict, Field, field_validator
from typing import Literal
class PostalAddress(BaseModel):
model_config = ConfigDict(extra="forbid")
street: str = Field(..., min_length=1)
city: str = Field(..., min_length=1)
state_or_province: str = Field(..., min_length=1)
postal_code: str = Field(..., min_length=1, pattern=r"^[A-Za-z0-9]+(?:[ -][A-Za-z0-9]+)*$")
country: str = Field(..., pattern=r"^[A-Z]{2}$")
address_type: Literal["residential", "commercial", "po_box"]
@field_validator("street", "city", "state_or_province", "postal_code", "country", mode="before")
@classmethod
def _strip_text(cls, v):
if not isinstance(v, str):
return v
return v.strip()