{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "geocoder",
	"title": "Geocoder",
	"type": "registry:ui",
	"description": "A geocoder component that provides search and autocomplete functionality for location-based queries. It can be used to find addresses, places, or points of interest based on user input.",
	"dependencies": [
		"@lucide/svelte@^0.554.0"
	],
	"devDependencies": [
		"@lucide/svelte@^0.544.0"
	],
	"registryDependencies": [
		"input"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { Input } from '$lib/components/ui/input';\n\timport { cn } from '$UTILS$';\n\timport { Check, Loader, MapPin, X } from '@lucide/svelte';\n\timport { fly } from 'svelte/transition';\n\timport type { Snippet } from 'svelte';\n\timport type { GeoLocation } from '.';\n\timport Highlight from './geocoder-highlight.svelte';\n\timport { formatAddress, type AddressConvention } from './format';\n\n\tlet {\n\t\tvalue = $bindable(''),\n\t\tselected = $bindable<GeoLocation | null>(null),\n\t\tloading = false,\n\t\tplaceholder = 'Search for a location...',\n\t\tlabelKey,\n\t\tformat = 'short',\n\t\tconvention = 'auto',\n\t\tshowCountry = true,\n\t\tlanguage,\n\t\tclass: className,\n\t\tonSelect,\n\t\tlocationSnippet,\n\t\temptySnippet,\n\t\t...rest\n\t}: {\n\t\tvalue: string;\n\t\tselected?: GeoLocation | null;\n\t\tloading?: boolean;\n\t\tplaceholder?: string;\n\t\tlabelKey?: keyof GeoLocation;\n\t\t/**\n\t\t * `short` builds a compact address from the structured fields, `full` uses\n\t\t * Nominatim's raw `display_name`, or pass a function for total control.\n\t\t */\n\t\tformat?: 'short' | 'full' | ((location: GeoLocation) => string);\n\t\t/** Address ordering. `auto` follows the country of each result. */\n\t\tconvention?: AddressConvention | 'auto';\n\t\tshowCountry?: boolean;\n\t\t/** Sent as `accept-language`, so results come back localised. */\n\t\tlanguage?: string;\n\t\tclass?: string;\n\t\tonSelect?: (loc: GeoLocation) => void;\n\t\tlocationSnippet?: Snippet<[GeoLocation, boolean]>;\n\t\temptySnippet?: Snippet;\n\t\t[key: string]: any;\n\t} = $props();\n\n\t/** The single source of truth for a result's label. */\n\tfunction label(loc: GeoLocation) {\n\t\tif (typeof format === 'function') return format(loc);\n\t\tif (format === 'full') return loc.display_name;\n\t\treturn formatAddress(loc, { convention, showCountry });\n\t}\n\n\tlet isOpen = $state(false);\n\tlet activeIndex = $state(-1);\n\n\tlet inputContainerRef = $state<HTMLDivElement | null>(null);\n\tlet listRef = $state<HTMLDivElement | null>(null);\n\n\tlet options: GeoLocation[] = $state([]);\n\n\tfunction open() {\n\t\tisOpen = true;\n\t\tactiveIndex = -1;\n\t}\n\n\tfunction close() {\n\t\tisOpen = false;\n\t\tactiveIndex = -1;\n\t}\n\n\tfunction handleInput(e: Event) {\n\t\tvalue = (e.target as HTMLInputElement).value;\n\t\tif (value.length > 0) open();\n\t\telse close();\n\t}\n\n\tfunction handleSelect(loc: GeoLocation) {\n\t\tselected = loc;\n\t\tvalue = labelKey ? String(loc[labelKey]) : label(loc);\n\n\t\tif (onSelect) onSelect(loc);\n\t\tclose();\n\t}\n\n\tfunction handleKeydown(e: KeyboardEvent) {\n\t\tif (!isOpen) {\n\t\t\tif (e.key === 'ArrowDown' && value.length > 0) open();\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (e.key) {\n\t\t\tcase 'ArrowDown':\n\t\t\t\te.preventDefault();\n\t\t\t\tactiveIndex = (activeIndex + 1) % options.length;\n\t\t\t\tscrollToActive();\n\t\t\t\tbreak;\n\t\t\tcase 'ArrowUp':\n\t\t\t\te.preventDefault();\n\t\t\t\tactiveIndex = (activeIndex - 1 + options.length) % options.length;\n\t\t\t\tscrollToActive();\n\t\t\t\tbreak;\n\t\t\tcase 'Enter':\n\t\t\t\te.preventDefault();\n\t\t\t\tif (activeIndex >= 0 && options[activeIndex]) {\n\t\t\t\t\thandleSelect(options[activeIndex]);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase 'Escape':\n\t\t\t\tclose();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tfunction scrollToActive() {\n\t\tif (!listRef) return;\n\t\tconst activeEl = listRef.children[activeIndex] as HTMLElement;\n\t\tif (activeEl) {\n\t\t\tactiveEl.scrollIntoView({ block: 'nearest' });\n\t\t}\n\t}\n\n\tfunction clickOutside(node: HTMLElement) {\n\t\tconst handleClick = (event: MouseEvent) => {\n\t\t\tconst target = event.target as Node;\n\t\t\tif (\n\t\t\t\tnode &&\n\t\t\t\t!node.contains(target) &&\n\t\t\t\tinputContainerRef &&\n\t\t\t\t!inputContainerRef.contains(target)\n\t\t\t) {\n\t\t\t\tclose();\n\t\t\t}\n\t\t};\n\t\tdocument.addEventListener('click', handleClick, true);\n\t\treturn {\n\t\t\tdestroy() {\n\t\t\t\tdocument.removeEventListener('click', handleClick, true);\n\t\t\t}\n\t\t};\n\t}\n\n\tlet debouncedQuery = $state('');\n\n\t$effect(() => {\n\t\tif (!value) {\n\t\t\tdebouncedQuery = '';\n\t\t\treturn;\n\t\t}\n\n\t\tconst handler = setTimeout(() => {\n\t\t\tdebouncedQuery = value;\n\t\t}, 500);\n\n\t\treturn () => clearTimeout(handler);\n\t});\n\n\t$effect(() => {\n\t\tif (debouncedQuery) {\n\t\t\tloading = true;\n\n\t\t\t// addressdetails=1 is what returns the structured `address` object the\n\t\t\t// short format is built from; without it only display_name comes back.\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tq: debouncedQuery,\n\t\t\t\tformat: 'json',\n\t\t\t\taddressdetails: '1'\n\t\t\t});\n\t\t\tif (language) params.set('accept-language', language);\n\n\t\t\tfetch(`https://nominatim.openstreetmap.org/search?${params}`)\n\t\t\t\t.then((res) => res.json())\n\t\t\t\t.then((data) => {\n\t\t\t\t\toptions = data;\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tconsole.error('Error fetching geocoding data', error);\n\t\t\t\t\toptions = [];\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tloading = false;\n\t\t\t\t});\n\t\t}\n\t});\n</script>\n\n<div class={cn('relative w-full group', className)}>\n\t<div class=\"relative\" bind:this={inputContainerRef}>\n\t\t<Input\n\t\t\ttype=\"text\"\n\t\t\t{placeholder}\n\t\t\tbind:value\n\t\t\toninput={handleInput}\n\t\t\tonkeydown={handleKeydown}\n\t\t\tonfocus={() => {\n\t\t\t\tif (value) open();\n\t\t\t}}\n\t\t\trole=\"combobox\"\n\t\t\taria-expanded={isOpen}\n\t\t\taria-autocomplete=\"list\"\n\t\t\t{...rest}\n\t\t/>\n\n\t\t{#if loading}\n\t\t\t<Loader\n\t\t\t\tclass=\"absolute right-3 top-1/2 -translate-y-1/2 size-4 animate-spin text-muted-foreground\"\n\t\t\t/>\n\t\t{:else if value.length > 0}\n\t\t\t<button\n\t\t\t\tclass=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n\t\t\t\tonclick={() => {\n\t\t\t\t\tvalue = '';\n\t\t\t\t\tselected = null;\n\t\t\t\t\tinputContainerRef?.querySelector('input')?.focus();\n\t\t\t\t\tclose();\n\t\t\t\t}}\n\t\t\t\taria-label=\"Clear\"\n\t\t\t>\n\t\t\t\t<X class=\"size-4\" />\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<MapPin\n\t\t\t\tclass=\"absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none\"\n\t\t\t/>\n\t\t{/if}\n\t</div>\n\n\t{#if isOpen}\n\t\t<div\n\t\t\tuse:clickOutside\n\t\t\tbind:this={listRef}\n\t\t\tclass=\"absolute z-50 mt-2 w-full max-h-[300px] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none\"\n\t\t\ttransition:fly={{ y: -5, duration: 150 }}\n\t\t>\n\t\t\t{#if options.length > 0}\n\t\t\t\t{#each options as option, i}\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\t\taria-selected={i === activeIndex}\n\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t'relative w-full flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',\n\t\t\t\t\t\t\ti === activeIndex\n\t\t\t\t\t\t\t\t? 'bg-accent text-accent-foreground'\n\t\t\t\t\t\t\t\t: 'hover:bg-accent hover:text-accent-foreground'\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tonclick={() => handleSelect(option)}\n\t\t\t\t\t\tonmouseenter={() => (activeIndex = i)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#if locationSnippet}\n\t\t\t\t\t\t\t{@render locationSnippet(option, option.place_id === selected?.place_id)}\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t<span class=\"flex w-full items-center gap-2 text-left\">\n\t\t\t\t\t\t\t\t<MapPin class=\"size-3.5 shrink-0 text-muted-foreground\" />\n\t\t\t\t\t\t\t\t<span class=\"truncate\">\n\t\t\t\t\t\t\t\t\t<Highlight text={label(option)} query={value} />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t{#if option.place_id === selected?.place_id}\n\t\t\t\t\t\t\t\t\t<Check class=\"ml-auto size-4 shrink-0 opacity-50\" />\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</button>\n\t\t\t\t{/each}\n\t\t\t{:else if loading}\n\t\t\t\t<div class=\"py-6 text-center text-sm text-muted-foreground\">\n\t\t\t\t\t<Loader\n\t\t\t\t\t\tclass=\"absolute right-3 top-1/2 -translate-y-1/2 size-4 animate-spin text-muted-foreground\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t{:else}\n\t\t\t\t<div class=\"py-6 text-center text-sm text-muted-foreground\">\n\t\t\t\t\t{#if emptySnippet}\n\t\t\t\t\t\t{@render emptySnippet()}\n\t\t\t\t\t{:else}\n\t\t\t\t\t\tNo results found.\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t{/if}\n\t\t</div>\n\t{/if}\n</div>\n",
			"type": "registry:ui",
			"target": "geocoder/geocoder.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\tlet { text, query, class: className }: { text: string; query: string; class?: string } = $props();\n\n\t// The query is raw user input, so it has to be escaped before it can be used\n\t// as a pattern — otherwise typing \"(\" throws a SyntaxError during render.\n\tconst escape = (input: string) => input.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n\tlet parts = $derived.by(() => {\n\t\tif (!query) return [{ text, highlight: false }];\n\t\tconst regex = new RegExp(`(${escape(query)})`, 'gi');\n\t\treturn text.split(regex).map((part) => ({\n\t\t\ttext: part,\n\t\t\thighlight: part.toLowerCase() === query.toLowerCase()\n\t\t}));\n\t});\n</script>\n\n<span class={className}>\n\t{#each parts as part}\n\t\t{#if part.highlight}\n\t\t\t<span class=\"bg-yellow-200 dark:bg-yellow-900/50 font-semibold text-foreground\"\n\t\t\t\t>{part.text}</span\n\t\t\t>\n\t\t{:else}\n\t\t\t{part.text}\n\t\t{/if}\n\t{/each}\n</span>\n",
			"type": "registry:ui",
			"target": "geocoder/geocoder-highlight.svelte"
		},
		{
			"content": "import type { Address, GeoLocation } from './index';\n\n/**\n * Address layouts differ along two independent axes: whether the house number\n * comes before or after the street, and whether the postcode comes before or\n * after the city. Three combinations cover essentially every Latin-script\n * convention in use.\n */\nexport type AddressConvention =\n\t/** `Musterstraße 12, 12345 Musterstadt` — most of Europe and Latin America. */\n\t| 'continental'\n\t/** `12 Rue de l'Exemple, 75000 Exempleville` — number first, postcode still before the city. */\n\t| 'french'\n\t/** `12 Example Street, Springfield 12345` — number first, postcode after the city. */\n\t| 'anglo';\n\nexport type FormatAddressOptions = {\n\t/** `auto` picks a convention from the result's country. */\n\tconvention?: AddressConvention | 'auto';\n\tshowCountry?: boolean;\n};\n\n/** Used for countries that are not mapped below. */\nexport const DEFAULT_CONVENTION: AddressConvention = 'anglo';\n\nexport const COUNTRY_CONVENTIONS: Record<string, AddressConvention> = {\n\t// Street then number, postcode then city.\n\tat: 'continental',\n\tde: 'continental',\n\tch: 'continental',\n\tli: 'continental',\n\tnl: 'continental',\n\tbe: 'continental',\n\tit: 'continental',\n\tes: 'continental',\n\tpt: 'continental',\n\tpl: 'continental',\n\tcz: 'continental',\n\tsk: 'continental',\n\thu: 'continental',\n\tsi: 'continental',\n\thr: 'continental',\n\trs: 'continental',\n\tba: 'continental',\n\tme: 'continental',\n\tmk: 'continental',\n\tal: 'continental',\n\tbg: 'continental',\n\tro: 'continental',\n\tgr: 'continental',\n\ttr: 'continental',\n\tse: 'continental',\n\tdk: 'continental',\n\tno: 'continental',\n\tfi: 'continental',\n\tis: 'continental',\n\tee: 'continental',\n\tlv: 'continental',\n\tlt: 'continental',\n\tru: 'continental',\n\tua: 'continental',\n\tby: 'continental',\n\tbr: 'continental',\n\tmx: 'continental',\n\tar: 'continental',\n\tcl: 'continental',\n\tco: 'continental',\n\tpe: 'continental',\n\tuy: 'continental',\n\tid: 'continental',\n\tvn: 'continental',\n\til: 'continental',\n\n\t// Number then street, postcode then city.\n\tfr: 'french',\n\tmc: 'french',\n\tlu: 'french',\n\n\t// Number then street, postcode after city.\n\tus: 'anglo',\n\tgb: 'anglo',\n\tca: 'anglo',\n\tau: 'anglo',\n\tnz: 'anglo',\n\tie: 'anglo',\n\tin: 'anglo',\n\tza: 'anglo',\n\tph: 'anglo',\n\tmy: 'anglo',\n\tsg: 'anglo',\n\thk: 'anglo',\n\tng: 'anglo',\n\tke: 'anglo',\n\tpk: 'anglo',\n\tbd: 'anglo',\n\tlk: 'anglo'\n};\n\n/**\n * Nominatim reports the settlement under whichever key matches its\n * administrative level, so the first populated one wins.\n */\nconst CITY_KEYS = ['city', 'town', 'village', 'municipality', 'hamlet', 'suburb'] as const;\n\nfunction pickCity(address: Address) {\n\tfor (const key of CITY_KEYS) {\n\t\tconst value = address[key];\n\t\tif (value) return value;\n\t}\n\treturn undefined;\n}\n\nexport function resolveConvention(\n\taddress: Address | undefined,\n\tconvention: AddressConvention | 'auto' = 'auto'\n): AddressConvention {\n\tif (convention !== 'auto') return convention;\n\n\tconst code = address?.country_code?.toLowerCase();\n\treturn (code && COUNTRY_CONVENTIONS[code]) || DEFAULT_CONVENTION;\n}\n\n/**\n * Turns a Nominatim result into a compact, locally ordered address such as\n * `Musterstraße 12, 12345 Musterstadt, Austria`.\n *\n * Requires the request to have been made with `addressdetails=1`; without the\n * structured `address` object this falls back to the raw `display_name`.\n */\nexport function formatAddress(location: GeoLocation, options: FormatAddressOptions = {}): string {\n\tconst { convention = 'auto', showCountry = true } = options;\n\n\tconst fallback = location?.display_name ?? '';\n\tconst address = location?.address;\n\tif (!address) return fallback;\n\n\tconst resolved = resolveConvention(address, convention);\n\tconst numberFirst = resolved !== 'continental';\n\tconst postcodeFirst = resolved !== 'anglo';\n\n\tconst street = address.road;\n\tconst number = address.house_number;\n\tconst city = pickCity(address);\n\tconst postcode = address.postcode;\n\n\tconst locality = (postcodeFirst ? [postcode, city] : [city, postcode]).filter(Boolean).join(' ');\n\n\t// Without a road this is a place rather than an address (a city, a landmark),\n\t// so lead with the object's own name — the first segment of display_name.\n\tlet primary = street\n\t\t? (numberFirst ? [number, street] : [street, number]).filter(Boolean).join(' ')\n\t\t: fallback.split(',')[0].trim();\n\n\t// Guard against \"Vienna, 1010 Vienna\" when the name is the settlement itself.\n\tif (!street && locality && primary && locality.includes(primary)) primary = '';\n\n\tconst parts = [primary, locality];\n\tif (showCountry && address.country) parts.push(address.country);\n\n\treturn parts.filter(Boolean).join(', ') || fallback;\n}\n",
			"type": "registry:ui",
			"target": "geocoder/format.ts"
		},
		{
			"content": "import Root from './geocoder.svelte';\nimport Highlight from './geocoder-highlight.svelte';\nimport {\n\tformatAddress,\n\tresolveConvention,\n\tCOUNTRY_CONVENTIONS,\n\tDEFAULT_CONVENTION,\n\ttype AddressConvention,\n\ttype FormatAddressOptions\n} from './format';\n\ntype GeoLocation = {\n\t/** Only present when the request was made with `addressdetails=1`. */\n\taddress?: Address;\n\tboundingbox: string[];\n\tclass: string;\n\tdisplay_name: string;\n\timportance: number;\n\tlat: string;\n\tlicence: string;\n\tlon: string;\n\tosm_id: number;\n\tosm_type: string;\n\tplace_id: number;\n\tsvg?: string;\n\ttype: string;\n};\n\n/**\n * Every field is conditional — Nominatim only returns the keys that apply to\n * the matched object and its administrative hierarchy.\n */\ntype Address = {\n\t'ISO3166-2-lvl4'?: string;\n\tborough?: string;\n\tcity?: string;\n\tcity_district?: string;\n\tcountry?: string;\n\tcountry_code?: string;\n\tcounty?: string;\n\thamlet?: string;\n\thistoric?: string;\n\thouse_number?: string;\n\tmunicipality?: string;\n\tneighbourhood?: string;\n\tpostcode?: string;\n\troad?: string;\n\tstate?: string;\n\tstate_district?: string;\n\tsuburb?: string;\n\ttown?: string;\n\tvillage?: string;\n};\n\nexport {\n\tRoot,\n\tHighlight,\n\tformatAddress,\n\tresolveConvention,\n\tCOUNTRY_CONVENTIONS,\n\tDEFAULT_CONVENTION,\n\ttype GeoLocation,\n\ttype Address,\n\ttype AddressConvention,\n\ttype FormatAddressOptions\n};\n",
			"type": "registry:ui",
			"target": "geocoder/index.ts"
		}
	]
}