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.json

Copies 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."
  }
}