All blocks

templates

Google Address Form

Google-powered address input; on pick the structured fields autofill from the Mapbase registry via points.resolve.

Live preview

Enter a Google Maps API key above to use Google autocomplete.

Address from Google Places
Search with Google — Mapbase resolves the canonical registry fields on select.

Pick a Google suggestion to autofill the address fields.

Related

Install

$bunx shadcn@latest add https://mapbase.dev/r/google-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/google-address-form.tsx
"use client"

import * as React from "react"
import {
  orderedHierarchy,
  type CountryCode,
  type PlaceSuggestion,
  type PointResolveNode,
  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 GoogleAddressFormProps = {
  apiKey: string
  baseUrl?: string
  googleApiKey: string
  country?: CountryCode
  onChange?: (fields: ResolvedAddressFields) => void
  className?: string
  disabled?: boolean
}

const EMPTY_FIELDS: ResolvedAddressFields = {
  line1: "",
  locality: "",
  admin: "",
  postcode: "",
  countryCode: "",
  locationId: null,
  centroid: null,
}

/**
 * Google Places autocomplete → Mapbase `points.resolve` → structured address
 * fields the user can wire into checkout or onboarding flows.
 */
export function GoogleAddressForm({
  apiKey,
  baseUrl,
  googleApiKey,
  country,
  onChange,
  className,
  disabled,
}: GoogleAddressFormProps) {
  return (
    <MapbaseProvider apiKey={apiKey} baseUrl={baseUrl} country={country}>
      <GoogleAddressFormInner
        apiKey={apiKey}
        baseUrl={baseUrl}
        googleApiKey={googleApiKey}
        country={country}
        onChange={onChange}
        className={className}
        disabled={disabled}
      />
    </MapbaseProvider>
  )
}

function GoogleAddressFormInner({
  apiKey,
  baseUrl,
  googleApiKey,
  country,
  onChange,
  className,
  disabled,
}: GoogleAddressFormProps) {
  const client = useMapbaseClient({ apiKey, baseUrl })
  const { resolveSuggestion, loading, error } = usePointResolve({
    apiKey,
    baseUrl,
    country,
    googleApiKey,
    source: "google",
    include: ["centroid", "bbox", "postcodes"],
  })

  const [selection, setSelection] = React.useState<PlaceSuggestion | null>(null)
  const [fields, setFields] =
    React.useState<ResolvedAddressFields>(EMPTY_FIELDS)
  const [taxonomy, setTaxonomy] = React.useState<Taxonomy | null>(null)

  const updateFields = (next: ResolvedAddressFields) => {
    setFields(next)
    onChange?.(next)
  }

  const handleSelect = async (suggestion: PlaceSuggestion) => {
    const resolved = await resolveSuggestion(suggestion)
    if (resolved) {
      setTaxonomy(resolved)
      updateFields(taxonomyToAddressFields(resolved, suggestion))
    } else {
      setTaxonomy(null)
      updateFields(EMPTY_FIELDS)
    }
  }

  return (
    <Card
      className={cn("w-full", className, disabled && "opacity-50")}
      data-1p-ignore
      data-form-type="other"
      data-lpignore="true"
    >
      <CardHeader>
        <CardTitle>Address from Google Places</CardTitle>
        <CardDescription>
          Search with Google — Mapbase resolves the canonical registry fields on
          select.
        </CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <Autocomplete
          apiKey={apiKey}
          baseUrl={baseUrl}
          disabled={disabled}
          source="google"
          googleApiKey={googleApiKey}
          country={country}
          variant="popover"
          triggerLabel="Search an address…"
          placeholder="Start typing an address…"
          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}
          taxonomy={taxonomy}
          clientReady={Boolean(client)}
        />
      </CardContent>
    </Card>
  )
}

function AddressFieldsPreview({
  fields,
  taxonomy,
  clientReady,
}: {
  fields: ResolvedAddressFields
  taxonomy: Taxonomy | null
  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 Google suggestion to autofill the address fields.
      </p>
    ) : null
  }

  const hierarchyNodes = taxonomy ? orderedHierarchy(taxonomy.hierarchy) : []

  return (
    <div className="grid gap-4 text-sm">
      <PreviewSection title="Address fields">
        <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
        />
      </PreviewSection>

      {taxonomy ? (
        <PreviewSection title="Taxonomy">
          <FieldRow
            label="Point"
            value={`${taxonomy.point[0].toFixed(7)}, ${taxonomy.point[1].toFixed(7)}`}
            mono
          />
          <FieldRow label="Country" value={taxonomy.country ?? "—"} mono />

          {taxonomy.anchor ? (
            <NodeBlock
              title="Anchor"
              node={taxonomy.anchor}
              extra={
                taxonomy.anchor.postcodes?.length ? (
                  <FieldRow
                    label="Postcodes"
                    value={taxonomy.anchor.postcodes.join(", ")}
                    mono
                  />
                ) : null
              }
            />
          ) : null}

          {hierarchyNodes.length > 0 ? (
            <div className="grid gap-2">
              <span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
                Hierarchy
              </span>
              {hierarchyNodes.map((node, index) => (
                <NodeBlock
                  key={node.id}
                  title={`Level ${index + 1}`}
                  node={node}
                />
              ))}
            </div>
          ) : null}

          {taxonomy.lau ? (
            <NodeBlock title="LAU" node={taxonomy.lau} />
          ) : null}

          {taxonomy.contains.length > 0 ? (
            <div className="grid gap-2">
              <span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
                Contains
              </span>
              {taxonomy.contains.map((node) => (
                <NodeBlock
                  key={node.id}
                  title={humanizeKind(node.kind)}
                  node={node}
                />
              ))}
            </div>
          ) : null}
        </PreviewSection>
      ) : null}
    </div>
  )
}

function PreviewSection({
  title,
  children,
}: {
  title: string
  children: React.ReactNode
}) {
  return (
    <div className="grid gap-3 rounded-md border bg-muted/30 p-3">
      <h4 className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
        {title}
      </h4>
      <div className="grid gap-2">{children}</div>
    </div>
  )
}

function NodeBlock({
  title,
  node,
  extra,
}: {
  title: string
  node: PointResolveNode
  extra?: React.ReactNode
}) {
  return (
    <div className="grid gap-1.5 rounded border border-border/60 bg-background/60 p-2">
      <div className="flex items-center justify-between gap-2">
        <span className="font-medium">{node.name}</span>
        <span className="rounded-full border px-2 py-0.5 text-[10px] text-muted-foreground">
          {title} · {humanizeKind(node.kind)}
        </span>
      </div>
      <NodeFieldRow label="ID" value={node.id} />
      <NodeFieldRow label="Layer" value={node.layer} />
      <NodeFieldRow label="Country" value={node.country} />
      {node.level != null ? (
        <NodeFieldRow label="Level" value={String(node.level)} />
      ) : null}
      {node.parent_id ? (
        <NodeFieldRow label="Parent ID" value={node.parent_id} />
      ) : null}
      {node.uri ? <NodeFieldRow label="URI" value={node.uri} /> : null}
      {node.centroid ? (
        <NodeFieldRow
          label="Centroid"
          value={`${node.centroid[0].toFixed(7)}, ${node.centroid[1].toFixed(7)}`}
        />
      ) : null}
      {extra}
    </div>
  )
}

function NodeFieldRow({ label, value }: { label: string; value: string }) {
  return (
    <div className="grid grid-cols-[5.5rem_1fr] gap-2 text-xs">
      <span className="text-muted-foreground">{label}</span>
      <span className="font-mono break-all">{value}</span>
    </div>
  )
}

function humanizeKind(kind: string): string {
  return kind
    .split("_")
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(" ")
}

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 extractPostalCodeFromLabel(label: string): string {
  const pt = label.match(/\b\d{4}-\d{3}\b/)
  if (pt) return pt[0]
  const generic = label.match(/\b[A-Z0-9]{2,}\s?\d[A-Z0-9]{2,}\b/i)
  return generic?.[0]?.trim() ?? ""
}

function taxonomyToAddressFields(
  taxonomy: Taxonomy,
  suggestion: PlaceSuggestion
): ResolvedAddressFields {
  console.log("suggestion", 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: suggestion.label.split(",")[0]?.trim() ?? anchor?.name ?? "",
    locality: lowest?.name ?? "",
    admin: adminNode?.name ?? "",
    postcode:
      anchor?.postcode ??
      postcodeNode?.name ??
      extractPostalCodeFromLabel(suggestion.label) ??
      "",
    countryCode: taxonomy.country ?? anchor?.country ?? "",
    locationId: anchor?.id ?? null,
    centroid: taxonomy.point ?? null,
  }
}

function humanizeErrorCode(code: MapbaseError["code"]): string {
  switch (code) {
    case "unauthorized":
      return "Invalid API key."
    case "rate_limited":
      return "Rate limited — try again shortly."
    case "not_found":
      return "No registry match for this place."
    case "network_error":
      return "Network error — check your connection."
    default:
      return "Something went wrong."
  }
}