Drag a map marker (or edit coordinates) to resolve WGS84 points via /v1/geocode/resolve. Shows ad_1..ad_4, hierarchy, and zones_detail.
/v1/geocode/resolve.bunx shadcn@latest add https://mapbase.dev/r/geocode-resolve-map.jsonThe shadcn CLI copies the file(s) below into your tree, installs npm dependencies, and recursively pulls registry dependencies (including from cross-registry URLs).
"use client"
import * as React from "react"
import {
Mapbase,
MapbaseError,
type CountryCode,
type GeocodeResolveResponse,
} from "mapbase"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Map,
MapControls,
MapMarker,
MarkerContent,
type MapViewport,
} from "@/components/ui/map"
import { cn } from "@/lib/utils"
export type GeocodeResolveMapProps = {
apiKey: string
baseUrl?: string
/** Initial marker position. Defaults to central Lisbon. */
initialLng?: number
initialLat?: number
/** Optional country filter forwarded to `/v1/geocode/resolve`. */
country?: CountryCode
/** Debounce window after drag or manual coord edits. Defaults to 350 ms. */
debounceMs?: number
/** Show manual longitude / latitude inputs. Defaults to true. */
showCoordInputs?: boolean
/** Fired when a resolve completes. */
onResolve?: (result: GeocodeResolveResponse | null) => void
className?: string
}
const DEFAULT_LNG = -9.1393
const DEFAULT_LAT = 38.7223
/**
* Interactive resolve playground: drag a marker (or type coordinates) and
* see the Mapbase administrative hierarchy for that WGS84 point.
*/
export function GeocodeResolveMap({
apiKey,
baseUrl,
initialLng = DEFAULT_LNG,
initialLat = DEFAULT_LAT,
country,
debounceMs = 350,
showCoordInputs = true,
onResolve,
className,
}: GeocodeResolveMapProps) {
const client = React.useMemo(
() => new Mapbase(apiKey, baseUrl ? { baseUrl } : undefined),
[apiKey, baseUrl],
)
const [lng, setLng] = React.useState(initialLng)
const [lat, setLat] = React.useState(initialLat)
const [lngInput, setLngInput] = React.useState(String(initialLng))
const [latInput, setLatInput] = React.useState(String(initialLat))
const [viewport, setViewport] = React.useState<MapViewport>({
center: [initialLng, initialLat],
zoom: 12,
bearing: 0,
pitch: 0,
})
const [result, setResult] = React.useState<GeocodeResolveResponse | null>(
null,
)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState<MapbaseError | null>(null)
const countryKey = country ?? ""
const runResolve = React.useCallback(
(nextLng: number, nextLat: number, signal: AbortSignal) => {
void (async () => {
setLoading(true)
setError(null)
const response = await client.geocode.resolve(
{
lng: nextLng,
lat: nextLat,
...(country ? { country } : {}),
},
{ signal },
)
if (signal.aborted) return
if (response.error) {
setError(response.error)
setResult(null)
onResolve?.(null)
} else {
setResult(response.data)
onResolve?.(response.data)
}
setLoading(false)
})()
},
[client, country, onResolve],
)
React.useEffect(() => {
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
const controller = new AbortController()
const timer = window.setTimeout(() => {
runResolve(lng, lat, controller.signal)
}, debounceMs)
return () => {
controller.abort()
window.clearTimeout(timer)
}
}, [lng, lat, countryKey, debounceMs, runResolve])
const handleDragEnd = React.useCallback(
({ lng: nextLng, lat: nextLat }: { lng: number; lat: number }) => {
setLng(nextLng)
setLat(nextLat)
setLngInput(nextLng.toFixed(6))
setLatInput(nextLat.toFixed(6))
setViewport((prev) => ({ ...prev, center: [nextLng, nextLat] }))
},
[],
)
const applyManualCoords = () => {
const nextLng = Number.parseFloat(lngInput)
const nextLat = Number.parseFloat(latInput)
if (!Number.isFinite(nextLng) || !Number.isFinite(nextLat)) return
setLng(nextLng)
setLat(nextLat)
setViewport((prev) => ({ ...prev, center: [nextLng, nextLat] }))
}
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader>
<CardTitle>Resolve on map</CardTitle>
<CardDescription>
Drag the marker or edit coordinates to resolve administrative
context via{" "}
<code className="font-mono text-xs">/v1/geocode/resolve</code>.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{showCoordInputs ? (
<div className="flex flex-wrap items-end gap-3">
<div className="flex min-w-[8rem] flex-col gap-1.5">
<Label htmlFor="geocode-resolve-lng" className="text-xs">
Longitude
</Label>
<Input
id="geocode-resolve-lng"
inputMode="decimal"
value={lngInput}
onChange={(e) => setLngInput(e.target.value)}
className="h-8 font-mono text-sm"
/>
</div>
<div className="flex min-w-[8rem] flex-col gap-1.5">
<Label htmlFor="geocode-resolve-lat" className="text-xs">
Latitude
</Label>
<Input
id="geocode-resolve-lat"
inputMode="decimal"
value={latInput}
onChange={(e) => setLatInput(e.target.value)}
className="h-8 font-mono text-sm"
/>
</div>
<Button
type="button"
variant="outline"
size="sm"
className="h-8"
onClick={applyManualCoords}
>
Apply
</Button>
</div>
) : null}
<div className="relative h-[320px] w-full overflow-hidden rounded-md border">
<Map
viewport={viewport}
onViewportChange={setViewport}
className="h-full w-full"
>
<MapControls position="bottom-right" showZoom />
<MapMarker
longitude={lng}
latitude={lat}
draggable
onDragEnd={handleDragEnd}
>
<MarkerContent>
<div className="size-3 rounded-full bg-primary ring-2 ring-background shadow-md" />
</MarkerContent>
</MapMarker>
</Map>
{loading ? (
<div className="absolute right-2 top-2 rounded-md bg-background/80 px-2 py-1 text-xs text-muted-foreground backdrop-blur">
Resolving…
</div>
) : null}
</div>
{error ? (
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm">
<p className="font-medium text-destructive">
{humanizeErrorCode(error.code)}
</p>
<p className="text-muted-foreground">{error.message}</p>
</div>
) : null}
{result ? <ResolveResultPanel result={result} /> : null}
</CardContent>
</Card>
)
}
function ResolveResultPanel({ result }: { result: GeocodeResolveResponse }) {
const adminRows = [
{
slot: "ad_1",
name: result.ad_1,
id: result.ad_1_id,
uri: result.ad_1_uri,
kind: result.ad_1_kind,
},
{
slot: "ad_2",
name: result.ad_2,
id: result.ad_2_id,
uri: result.ad_2_uri,
kind: result.ad_2_kind,
},
{
slot: "ad_3",
name: result.ad_3,
id: result.ad_3_id,
uri: result.ad_3_uri,
kind: result.ad_3_kind,
},
{
slot: "ad_4",
name: result.ad_4,
id: result.ad_4_id,
uri: result.ad_4_uri,
kind: result.ad_4_kind,
},
]
return (
<div className="flex flex-col gap-3 rounded-md border bg-muted/40 p-3 text-sm">
<div>
<div className="font-medium">{result.anchor.name}</div>
<div className="text-xs text-muted-foreground">
{result.country}
{result.coord
? ` · ${result.coord[0].toFixed(6)}, ${result.coord[1].toFixed(6)}`
: null}
</div>
</div>
<div className="grid gap-1 text-xs">
{adminRows.map((row) =>
row.name ? (
<div key={row.slot} className="grid grid-cols-[3rem_1fr] gap-2">
<span className="font-mono text-muted-foreground">
{row.slot}
</span>
<span>
{row.name}
{row.id ? (
<span className="text-muted-foreground"> · {row.id}</span>
) : null}
{row.uri ? (
<span className="text-muted-foreground"> · {row.uri}</span>
) : null}
{row.kind ? (
<span className="text-muted-foreground"> · {row.kind}</span>
) : null}
</span>
</div>
) : null,
)}
</div>
{result.zones.length > 0 ? (
<p className="text-xs text-muted-foreground">
Zones: {result.zones.join(", ")}
</p>
) : null}
{result.hierarchy.length > 0 ? (
<div className="overflow-x-auto rounded border bg-background/60">
<table className="w-full text-left text-xs">
<thead>
<tr className="border-b text-muted-foreground">
<th className="px-2 py-1.5 font-medium">Depth</th>
<th className="px-2 py-1.5 font-medium">Name</th>
<th className="px-2 py-1.5 font-medium">Kind</th>
</tr>
</thead>
<tbody>
{result.hierarchy.map((node) => (
<tr key={node.id} className="border-b last:border-0">
<td className="px-2 py-1.5 tabular-nums">{node.depth}</td>
<td className="px-2 py-1.5">{node.name}</td>
<td className="px-2 py-1.5">{node.kind}</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
{result.zones_detail.length > 0 ? (
<div className="overflow-x-auto rounded border bg-background/60">
<p className="border-b px-2 py-1.5 text-xs font-medium">
zones_detail
</p>
<table className="w-full text-left text-xs">
<tbody>
{result.zones_detail.map((zone) => (
<tr key={zone.id} className="border-b last:border-0">
<td className="px-2 py-1.5 tabular-nums">{zone.depth}</td>
<td className="px-2 py-1.5">{zone.name}</td>
<td className="px-2 py-1.5">{zone.kind}</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</div>
)
}
function humanizeErrorCode(code: MapbaseError["code"]): string {
switch (code) {
case "unauthorized":
return "Invalid API key"
case "rate_limited":
return "Rate limited — try again shortly"
case "invalid_request":
return "Invalid coordinates"
case "not_found":
return "No location at this point"
case "not_implemented":
return "Endpoint not yet available"
case "network_error":
return "Network error"
default:
return "Something went wrong"
}
}