All blocks
templates
Postcode Address Form
Postcode autocomplete with Mapbase registry layer postcodes; on pick structured address fields autofill via points.resolve.
Open playground →Live preview
Address from postcode
Search a postal code — Mapbase resolves the canonical address fields on select.
Pick a postcode suggestion to autofill the address fields.
Related
Install
$
bunx shadcn@latest add https://mapbase.dev/r/postcode-address-form.jsonCopies source into your app and installs npm + registry dependencies.
npm dependencies
mapbasemotion@tanstack/react-query
Registry dependencies
cardhttps://mapbase.dev/r/mapbase-provider.jsonhttps://mapbase.dev/r/autocomplete.json
Source
components/mapbase/postcode-address-form.tsx
"use client"
import * as React from "react"
import {
orderedHierarchy,
type CountryCode,
type LocationSuggestion,
type PlaceSuggestion,
type Taxonomy,
MapbaseError,
} from "mapbase"
import { useMapbaseClient, usePointResolve } from "mapbase/react"
import { Autocomplete } from "@/components/mapbase/autocomplete"
import { MapbaseProvider } from "@/components/mapbase/mapbase-provider"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { cn } from "@/lib/utils"
export type ResolvedAddressFields = {
line1: string
locality: string
admin: string
postcode: string
countryCode: string
locationId: string | null
centroid: [number, number] | null
}
export type PostcodeAddressFormProps = {
apiKey: string
baseUrl?: string
defaultCountry?: CountryCode
onChange?: (fields: ResolvedAddressFields) => void
className?: string
disabled?: boolean
}
const EMPTY_FIELDS: ResolvedAddressFields = {
line1: "",
locality: "",
admin: "",
postcode: "",
countryCode: "",
locationId: null,
centroid: null,
}
/**
* Postcode autocomplete → Mapbase `points.resolve` → structured address fields.
*/
export function PostcodeAddressForm({
apiKey,
baseUrl,
defaultCountry = "PT",
onChange,
className,
disabled,
}: PostcodeAddressFormProps) {
return (
<MapbaseProvider apiKey={apiKey} baseUrl={baseUrl} country={defaultCountry}>
<PostcodeAddressFormInner
apiKey={apiKey}
baseUrl={baseUrl}
defaultCountry={defaultCountry}
onChange={onChange}
className={className}
disabled={disabled}
/>
</MapbaseProvider>
)
}
function PostcodeAddressFormInner({
apiKey,
baseUrl,
defaultCountry = "PT",
onChange,
className,
disabled,
}: PostcodeAddressFormProps) {
const client = useMapbaseClient({ apiKey, baseUrl })
const { resolveSuggestion, loading, error } = usePointResolve({
apiKey,
baseUrl,
country: defaultCountry,
})
const [selection, setSelection] = React.useState<PlaceSuggestion | null>(null)
const [fields, setFields] =
React.useState<ResolvedAddressFields>(EMPTY_FIELDS)
const updateFields = (next: ResolvedAddressFields) => {
setFields(next)
onChange?.(next)
}
const handleSelect = async (suggestion: PlaceSuggestion) => {
const resolved = await resolveSuggestion(suggestion)
if (resolved) {
updateFields(taxonomyToAddressFields(resolved, suggestion))
} else {
updateFields(postcodeOnlyFields(suggestion))
}
}
return (
<Card
className={cn("w-full", className, disabled && "opacity-50")}
data-1p-ignore
data-form-type="other"
data-lpignore="true"
>
<CardHeader>
<CardTitle>Address from postcode</CardTitle>
<CardDescription>
Search a postal code — Mapbase resolves the canonical address fields on
select.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Autocomplete
apiKey={apiKey}
baseUrl={baseUrl}
disabled={disabled}
country={defaultCountry}
layers={["postcodes"]}
variant="popover"
triggerLabel="Search a postal code…"
placeholder="Start typing a postal code…"
value={selection}
onChange={setSelection}
onSelect={handleSelect}
/>
{loading ? (
<p className="text-sm text-muted-foreground">
Resolving with Mapbase…
</p>
) : null}
{error ? (
<p className="text-sm text-destructive">
{humanizeErrorCode(error.code)}
</p>
) : null}
<AddressFieldsPreview fields={fields} clientReady={Boolean(client)} />
</CardContent>
</Card>
)
}
function AddressFieldsPreview({
fields,
clientReady,
}: {
fields: ResolvedAddressFields
clientReady: boolean
}) {
const hasData =
fields.line1 ||
fields.locality ||
fields.admin ||
fields.postcode ||
fields.countryCode
if (!hasData) {
return clientReady ? (
<p className="text-sm text-muted-foreground">
Pick a postcode suggestion to autofill the address fields.
</p>
) : null
}
return (
<div className="grid gap-3 rounded-md border bg-muted/30 p-3 text-sm">
<FieldRow label="Line 1" value={fields.line1} />
<FieldRow label="Locality" value={fields.locality} />
<FieldRow label="Admin area" value={fields.admin} />
<FieldRow label="Postcode" value={fields.postcode} />
<FieldRow label="Country" value={fields.countryCode} />
<FieldRow label="Location ID" value={fields.locationId ?? "—"} mono />
<FieldRow
label="Centroid"
value={
fields.centroid
? `${fields.centroid[0].toFixed(4)}, ${fields.centroid[1].toFixed(4)}`
: "—"
}
mono
/>
</div>
)
}
function FieldRow({
label,
value,
mono,
}: {
label: string
value: string
mono?: boolean
}) {
return (
<div className="grid grid-cols-[7rem_1fr] gap-2">
<span className="text-muted-foreground">{label}</span>
<span className={mono ? "font-mono text-xs" : ""}>{value || "—"}</span>
</div>
)
}
function suggestionRow(
suggestion: PlaceSuggestion,
): LocationSuggestion | undefined {
return suggestion.ref as LocationSuggestion | undefined
}
function postcodeOnlyFields(
suggestion: PlaceSuggestion,
): ResolvedAddressFields {
const row = suggestionRow(suggestion)
return {
...EMPTY_FIELDS,
postcode: row?.name ?? suggestion.label,
countryCode: row?.country ?? "",
centroid: row?.centroid ?? null,
}
}
function taxonomyToAddressFields(
taxonomy: Taxonomy,
suggestion: PlaceSuggestion,
): ResolvedAddressFields {
const row = suggestionRow(suggestion)
const nodes = orderedHierarchy(taxonomy.hierarchy)
const anchor = taxonomy.anchor
const lowest = nodes[nodes.length - 1]
const adminNode =
nodes.find(
(n) =>
n.kind === "province" ||
n.kind === "district" ||
n.kind === "autonomous_region" ||
n.kind === "country",
) ?? nodes[Math.max(0, nodes.length - 2)]
const postcodeNode = nodes.find((n) => n.layer === "postcodes")
return {
line1: anchor?.name ?? lowest?.name ?? "",
locality: lowest?.name ?? "",
admin: adminNode?.name ?? "",
postcode: row?.name ?? postcodeNode?.name ?? "",
countryCode: taxonomy.country ?? anchor?.country ?? row?.country ?? "",
locationId: anchor?.id ?? null,
centroid: taxonomy.point ?? row?.centroid ?? null,
}
}
function humanizeErrorCode(code: MapbaseError["code"] | string): string {
switch (code) {
case "not_found":
return "No registry match for this postcode."
case "unauthorized":
return "Invalid API key."
case "rate_limited":
return "Rate limited — try again shortly."
case "invalid_request":
return "Invalid postcode format."
case "network_error":
return "Network error — check your connection."
default:
return "Something went wrong."
}
}