Search and routing
The geocoder and the routing panel are built on your own shadcn primitives and call the Geocoding API and the Routing API. Every call they make is one request, metered like any other. well-search and legal-land-search are the geocoder with other labels, on Overlay components.
npx shadcn@latest registry add @unmap=https://registry.unmap.dev/r/{name}.json
npx shadcn@latest add @unmap/geocodernpx shadcn-vue@latest add https://registry.unmap.dev/r/vue/geocoder.jsonThe React line registers the registry once per project, so later items can be named @unmap/<item>; shadcn-vue has no registry add and takes the item URL directly. Files land in components/ui/ beside your own; nothing is overwritten without asking.
Browse every item on registry.unmap.dev| Item | What it is |
|---|---|
geocoder | A search box with debounced autocomplete that flies the map to the chosen place. Typed coordinates are reverse geocoded; bbox, proximity and your own result layout are optional. |
routing-panel | From/to inputs that resolve both addresses, request a route, and draw it on the map. |
Search box options
The geocoder reverse geocodes coordinates typed into it, and lists the addresses nearest the point.
It reads lat, lng by default, so 51.05, -114.07 is Calgary. A negative first number with a
positive second means lng, lat, so -114.07, 51.05 is Calgary too: everywhere in Canada the
latitude is positive and the longitude negative. A French decimal comma works when a space or ;
separates the two numbers, as in 51,05 -114,07, while a lone 51,05 is a text search. The
Unicode minus sign counts as -. Degrees, minutes and seconds stay a text search. A point outside
Canada says so, in English or French to match lang. Every lookup is one request, like a search.
reverseGeocode={false} searches coordinates as text instead. well-search and
legal-land-search start with it off, since coordinates in a UWI or LSD box are not a request for
an address; pass reverseGeocode (Vue: :reverse-geocode="true") to turn it back on.
bbox([west, south, east, north]) keeps results inside a rectangle. The geocoder filters in the browser, because autocomplete has no server-side box: it asks for more suggestions thanlimitand drops the ones outside, so a box can show fewer thanlimit. A typed coordinate is looked up wherever it is, and the box does not filter it.proximityprefers results near a point without excluding any:[lng, lat], or"map"for the map's centre at the moment each search fires. It is sent to the gateway asfocus.renderResult(Vue: theresultslot) draws each option.emptyText(Vue:empty-text) replaces what the list says when a search finds nothing.errorText(Vue:error-text) is a function from the error to what the list says when a search fails, the outside-Canada line included; a gateway error carriesstatusandcode.well-searchandlegal-land-searchtake both. The "Searching..." line followslang.
"use client";
import { Geocoder } from "@/components/ui/geocoder";
import { Map } from "@/components/ui/map";
export function PlaceSearch() {
const apiKey = process.env.NEXT_PUBLIC_UNMAP_KEY!;
return (
<div className="h-[560px]">
<Map apiKey={apiKey} center={[-114.07, 51.05]} zoom={10}>
<div className="absolute top-3 left-3 z-10 w-72">
<Geocoder
apiKey={apiKey}
bbox={[-115.2, 50.8, -113.8, 51.4]}
proximity="map"
renderResult={(result) => (
<span className="flex flex-col">
<span>{result.name}</span>
<span className="text-xs text-muted-foreground">{result.address?.region}</span>
</span>
)}
/>
</div>
</Map>
</div>
);
}<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapGeocoder } from "@/components/ui/geocoder";
const apiKey = import.meta.env.VITE_UNMAP_KEY;
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="10">
<div class="absolute top-3 left-3 z-10 w-72">
<UnmapGeocoder :api-key="apiKey" :bbox="[-115.2, 50.8, -113.8, 51.4]" proximity="map">
<template #result="{ result }">
<span class="flex flex-col">
<span>{{ result.name }}</span>
<span class="text-xs text-muted-foreground">{{ result.address?.region }}</span>
</span>
</template>
</UnmapGeocoder>
</div>
</UnmapMap>
</template>Routing panel
routing-panel resolves a from and a to address with the geocoder, asks the
Routing API for a route between them, and draws it with route-line, which
installs with it. A submit is two searches and a route, so three requests. mode is auto (the
default), car, bicycle, pedestrian or truck, and onRoute (Vue: @route) receives each
route it draws: the line, its distance in metres and its duration in seconds. It renders a bare
form, so the example floats it in your Card. Elevation profile
hands that line to elevation-profile.
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Map } from "@/components/ui/map";
import { RoutingPanel } from "@/components/ui/routing-panel";
export function Directions() {
const apiKey = process.env.NEXT_PUBLIC_UNMAP_KEY!;
return (
<div className="h-[560px]">
<Map apiKey={apiKey} center={[-114.07, 51.05]} zoom={11}>
<Card className="absolute top-3 left-3 z-10 w-80">
<CardContent>
<RoutingPanel
apiKey={apiKey}
mode="bicycle"
onRoute={(route) => console.log(route.distanceMeters, route.durationSeconds)}
/>
</CardContent>
</Card>
</Map>
</div>
);
}<script setup lang="ts">
import type { LngLat } from "@unmap/routing";
import { Card, CardContent } from "@/components/ui/card";
import { UnmapMap } from "@/components/ui/map";
import { UnmapRoutingPanel } from "@/components/ui/routing-panel";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
function onRoute(route: { coordinates: LngLat[]; distanceMeters: number; durationSeconds: number }) {
console.log(route.distanceMeters, route.durationSeconds);
}
</script>
<template>
<div class="h-[560px]">
<UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="11">
<Card class="absolute top-3 left-3 z-10 w-80">
<CardContent>
<UnmapRoutingPanel :api-key="apiKey" mode="bicycle" @route="onRoute" />
</CardContent>
</Card>
</UnmapMap>
</div>
</template>