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