diff --git a/components/src/index.js b/components/src/index.js
index 06dd7691..a3e4ba1d 100644
--- a/components/src/index.js
+++ b/components/src/index.js
@@ -29,6 +29,7 @@ export { default as Map } from './maplibre/Map/Map.svelte';
export { SWRDataLabLight, SWRDataLabDark } from './maplibre/MapStyle';
export { default as MapControl } from './maplibre/MapControl/MapControl.svelte';
export { default as AttributionControl } from './maplibre/AttributionControl/AttributionControl.svelte';
+export { default as Geocoder } from './maplibre/Geocoder/Geocoder.svelte';
export { default as GeocoderControl } from './maplibre/GeocoderControl/GeocoderControl.svelte';
export { default as NavigationControl } from './maplibre/NavigationControl/NavigationControl.svelte';
export { default as ScaleControl } from './maplibre/ScaleControl/ScaleControl.svelte';
diff --git a/components/src/maplibre/Geocoder/Geocoder.mdx b/components/src/maplibre/Geocoder/Geocoder.mdx
new file mode 100644
index 00000000..29f5e713
--- /dev/null
+++ b/components/src/maplibre/Geocoder/Geocoder.mdx
@@ -0,0 +1,10 @@
+import { Story, Meta, Primary, Controls, Stories } from '@storybook/addon-docs/blocks';
+
+import * as GeocoderStories from './Geocoder.stories.svelte';
+
+
+
+# Geocoder
+
+
+
diff --git a/components/src/maplibre/Geocoder/Geocoder.stories.svelte b/components/src/maplibre/Geocoder/Geocoder.stories.svelte
new file mode 100644
index 00000000..54d9b6d9
--- /dev/null
+++ b/components/src/maplibre/Geocoder/Geocoder.stories.svelte
@@ -0,0 +1,51 @@
+
+
+ {
+ }}
+>
+
+
+
+
+
+
diff --git a/components/src/maplibre/Geocoder/Geocoder.svelte b/components/src/maplibre/Geocoder/Geocoder.svelte
new file mode 100644
index 00000000..7e3aac5a
--- /dev/null
+++ b/components/src/maplibre/Geocoder/Geocoder.svelte
@@ -0,0 +1,60 @@
+
+
+
+
+
diff --git a/components/src/maplibre/Geocoder/GeocoderAPIs.ts b/components/src/maplibre/Geocoder/GeocoderAPIs.ts
new file mode 100644
index 00000000..5cd82931
--- /dev/null
+++ b/components/src/maplibre/Geocoder/GeocoderAPIs.ts
@@ -0,0 +1,62 @@
+import {
+ type CarmenGeojsonFeature,
+ type MaplibreGeocoderApi,
+ type MaplibreGeocoderApiConfig,
+ type MaplibreGeocoderFeatureResults
+} from '@maplibre/maplibre-gl-geocoder';
+
+export class MaptilerGeocoderAPI implements MaplibreGeocoderApi {
+ key: string;
+
+ constructor(key: string) {
+ this.key = key;
+ }
+
+ async forwardGeocode(config: MaplibreGeocoderApiConfig): Promise {
+ const result: MaplibreGeocoderFeatureResults = {
+ type: 'FeatureCollection',
+ features: []
+ };
+ try {
+ const params = new URLSearchParams(
+ Object.fromEntries(
+ Object.entries({
+ country: config.countries || '',
+ language: config.language || '',
+ types: config.types || '',
+ key: this.key || ''
+ }).filter(([_, value]) => {
+ return value !== '';
+ })
+ )
+ );
+
+ const response = await fetch(
+ `https://api.maptiler.com/geocoding/${config.query}.json?${params.toString()}`
+ );
+ const geojson = await response.json();
+ for (const feature of geojson.features) {
+ const res: CarmenGeojsonFeature = {
+ type: 'Feature',
+ id: feature.id,
+ place_type: ['place'],
+ place_name: feature.place_name,
+ language: feature.properties.display_name,
+ text: feature.text,
+ bbox: feature.bbox,
+ geometry: {
+ type: 'Point',
+ coordinates: feature.center
+ },
+ properties: {
+ text: feature.text
+ }
+ };
+ result.features.push(res);
+ }
+ } catch (e) {
+ console.error(`Failed to forwardGeocode with error: ${e}`);
+ }
+ return result;
+ }
+}
diff --git a/components/src/maplibre/Geocoder/GeocoderProps.ts b/components/src/maplibre/Geocoder/GeocoderProps.ts
new file mode 100644
index 00000000..c19633f4
--- /dev/null
+++ b/components/src/maplibre/Geocoder/GeocoderProps.ts
@@ -0,0 +1,37 @@
+import { Map } from "maplibre-gl";
+import {
+ type GeocodingCountry,
+ type GeocodingLanguage,
+ type GeocodingPlaceType,
+ type GeocodingService
+ } from '../types';
+
+
+export default interface GeocoderProps {
+ map?: Map | null
+ service: GeocodingService;
+ /**
+ * API key for selected geocoding `service`
+ */
+ key: string;
+ /**
+ * Limit the number of returned suggestions
+ */
+ limit?: number;
+ /**
+ * Limit search to one or more countries
+ */
+ countries?: GeocodingCountry | GeocodingCountry[];
+ /**
+ * Limit search to one or more result type.
+ */
+ types?: GeocodingPlaceType | GeocodingPlaceType[];
+ /**
+ * Limit search to one or more languages. The UI is localised to the first language specified if [available](https://github.com/maplibre/maplibre-gl-geocoder/blob/main/lib/localization.ts).
+ */
+ languages?: GeocodingLanguage | GeocodingLanguage[];
+ /**
+ * Overwrite the default input placeholder
+ */
+ placeholder?: string;
+ }
diff --git a/components/src/maplibre/Geocoder/geocoder.scss b/components/src/maplibre/Geocoder/geocoder.scss
new file mode 100644
index 00000000..91b7cd69
--- /dev/null
+++ b/components/src/maplibre/Geocoder/geocoder.scss
@@ -0,0 +1,162 @@
+ :global {
+ .maplibregl-ctrl-geocoder {
+ background-color: var(--color-pageFill);
+ position: relative;
+ width: 100%;
+ z-index: 1;
+ font-family: var(--swr-sans);
+ font-size: var(--fs-base);
+ border: 1px solid var(--color-textSecondary);
+
+ button {
+ cursor: pointer;
+ }
+ * {
+ box-sizing: border-box;
+ }
+ }
+
+ .maplibre-gl-geocoder--error {
+ font-size: var(--fs-small-2);
+ color: var(--color-textSecondary);
+ padding: 0.4em 0.65em;
+ }
+
+ .maplibregl-ctrl-geocoder--input {
+ height: 100%;
+ font-family: inherit;
+ font-size: inherit;
+ background-color: transparent;
+ color: var(--color-textPrimary);
+ border: 0;
+ height: 2em;
+ padding: 0 1.85em;
+ width: 100%;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ &::-webkit-search-cancel-button,
+ &::-webkit-search-decoration {
+ appearance: none;
+ }
+ }
+
+ .maplibregl-ctrl-geocoder--input::placeholder {
+ opacity: 1;
+ color: var(--color-textSecondary);
+ }
+ .maplibregl-ctrl-geocoder--input:focus {
+ color: var(--color-textPrimary);
+ outline: 0;
+ }
+
+ .maplibregl-ctrl-geocoder .maplibregl-ctrl-geocoder--pin-right > * {
+ display: none;
+ position: absolute;
+ z-index: 2;
+ right: 0.5em;
+ top: 50%;
+ transform: translateY(-50%);
+ border: 0;
+ }
+
+ /* Suggestions */
+ .maplibregl-ctrl-geocoder .suggestions {
+ background: var(--color-pageFill);
+ border: 1px solid var(--color-textSecondary);
+ left: -1px;
+ list-style: none;
+ position: absolute;
+ width: calc(100% + 2px);
+ top: 100%;
+ z-index: 1000;
+ overflow: hidden;
+ }
+
+ .maplibregl-ctrl-bottom-left .suggestions,
+ .maplibregl-ctrl-bottom-right .suggestions {
+ top: auto;
+ bottom: 100%;
+ }
+
+ .maplibregl-ctrl-geocoder .suggestions > li > a {
+ cursor: default;
+ display: block;
+ font-size: var(--fs-small-1);
+ padding: 0.7em 0.75em;
+ padding-bottom: 0.65em;
+ border-bottom: 1px solid var(--color-surfaceBorder);
+ }
+ .maplibregl-ctrl-geocoder .suggestions > li:last-child > a {
+ border-bottom: 0;
+ }
+
+ .maplibregl-ctrl-geocoder .suggestions > .active > a,
+ .maplibregl-ctrl-geocoder .suggestions > li > a:hover {
+ background: var(--color-surfaceHover);
+ text-decoration: none;
+ cursor: pointer;
+ }
+ .maplibregl-ctrl-geocoder .suggestions > .active .maplibregl-ctrl-geocoder--result-title,
+ .maplibregl-ctrl-geocoder .suggestions > li > a:hover .maplibregl-ctrl-geocoder--result-title {
+ text-decoration: underline;
+ }
+
+ .maplibregl-ctrl-geocoder--suggestion {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ .maplibregl-ctrl-geocoder--suggestion-info {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .maplibregl-ctrl-geocoder--suggestion-title,
+ .maplibregl-ctrl-geocoder--suggestion-address,
+ .maplibregl-ctrl-geocoder--result-address {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ .maplibregl-ctrl-geocoder--result-icon {
+ display: none;
+ }
+ .maplibregl-ctrl-geocoder--result-title {
+ color: var(--color-textPrimary);
+ font-weight: 600;
+ letter-spacing: 0;
+ }
+ .maplibregl-ctrl-geocoder--result-address {
+ font-size: var(--fs-small-2);
+ color: var(--color-textSecondary);
+ line-height: 1.3;
+ }
+
+ .maplibregl-ctrl-geocoder--icon {
+ display: inline-block;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ path {
+ fill: var(--color-textPrimary);
+ }
+ }
+
+ .maplibregl-ctrl-geocoder--icon-close {
+ right: .25em;
+ height: 1.15em;
+ opacity: 0.65;
+ &:hover, &:focus-visible {
+ opacity: 1;
+ }
+ }
+ .maplibregl-ctrl-geocoder--icon-search {
+ left: 0.4em;
+ height: 1.25em;
+ top: 51%; // optical correction
+ width: auto;
+ }
+ }
diff --git a/components/src/maplibre/Geocoder/index.js b/components/src/maplibre/Geocoder/index.js
new file mode 100644
index 00000000..ef694f5f
--- /dev/null
+++ b/components/src/maplibre/Geocoder/index.js
@@ -0,0 +1,2 @@
+import Geocoder from './Geocoder.svelte';
+export default Geocoder;
diff --git a/components/src/maplibre/GeocoderControl/GeocoderControl.mdx b/components/src/maplibre/GeocoderControl/GeocoderControl.mdx
index b4d13201..4527fefb 100644
--- a/components/src/maplibre/GeocoderControl/GeocoderControl.mdx
+++ b/components/src/maplibre/GeocoderControl/GeocoderControl.mdx
@@ -14,11 +14,13 @@ import * as GeocoderControlStories from './GeocoderControl.stories.svelte';
Maptiler doesn't allow filtering by German admin boundaries, but its `types` seem to map as follows:
-
+
+
| type |
boundary |
-
+
| county | Kreis |
| municipality | Gemeinde |
| region | Bundesland |
+
diff --git a/components/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte b/components/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte
index 31c2f53f..36862a7c 100644
--- a/components/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte
+++ b/components/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte
@@ -35,7 +35,6 @@