> ## Documentation Index
> Fetch the complete documentation index at: https://algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# geoSearch

> Lets users perform location-based searches inside a bounding box on a map.

export const FlavorSwitcher = ({current, baseHref = "", options = [], label = "InstantSearch framework"}) => {
  if (options.length === 0) {
    return <div className="not-prose" role="alert" style={{
      margin: "0.25rem 0 1.5rem",
      padding: "0.75rem",
      border: "1px solid #f59e0b",
      borderRadius: "0.625rem",
      color: "inherit",
      fontSize: "0.875rem"
    }}>
        FlavorSwitcher requires at least one option.
      </div>;
  }
  const selected = options.find(option => option.value === current) ?? options[0];
  return <div className="not-prose mint-flavor-switcher">
      <style>{`
        .mint-flavor-switcher {
          --mfs-bg: #ffffff;
          --mfs-bg-hover: #f4f4f5;
          --mfs-bg-current: #eef2ff;
          --mfs-border: #d4d4d8;
          --mfs-fg: #18181b;
          --mfs-muted: #71717a;
          --mfs-accent: #4f46e5;
          position: relative;
          width: min(100%, 19rem);
          margin: 0.25rem 0 1.5rem;
          color: var(--mfs-fg);
          font-size: 0.875rem;
          line-height: 1.25rem;
        }

        .dark .mint-flavor-switcher {
          --mfs-bg: #18181b;
          --mfs-bg-hover: #27272a;
          --mfs-bg-current: #272747;
          --mfs-border: #3f3f46;
          --mfs-fg: #fafafa;
          --mfs-muted: #a1a1aa;
          --mfs-accent: #a5b4fc;
        }

        .mint-flavor-switcher details {
          position: relative;
        }

        .mint-flavor-switcher summary {
          display: flex;
          min-height: 2.75rem;
          box-sizing: border-box;
          align-items: center;
          justify-content: space-between;
          gap: 0.75rem;
          padding: 0.625rem 0.75rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          color: var(--mfs-fg);
          cursor: pointer;
          font-weight: 600;
          list-style: none;
          transition: border-color 150ms ease, box-shadow 150ms ease;
        }

        .mint-flavor-switcher summary::-webkit-details-marker {
          display: none;
        }

        .mint-flavor-switcher summary:hover {
          border-color: var(--mfs-accent);
        }

        .mint-flavor-switcher summary:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: 2px;
        }

        .mint-flavor-switcher__label {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }

        .mint-flavor-switcher__chevron {
          flex: none;
          transition: transform 150ms ease;
        }

        .mint-flavor-switcher details[open] .mint-flavor-switcher__chevron {
          transform: rotate(180deg);
        }

        .mint-flavor-switcher__menu {
          position: absolute;
          z-index: 50;
          top: calc(100% + 0.375rem);
          left: 0;
          width: 100%;
          box-sizing: border-box;
          margin: 0;
          padding: 0.375rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          box-shadow: 0 12px 30px rgb(0 0 0 / 16%);
          list-style: none;
        }

        .mint-flavor-switcher__menu li {
          margin: 0;
          padding: 0;
        }

        .mint-flavor-switcher__option {
          display: grid;
          gap: 0.125rem;
          padding: 0.625rem 0.75rem;
          border-radius: 0.4rem;
          color: var(--mfs-fg);
          text-decoration: none;
        }

        .mint-flavor-switcher__option:hover {
          background: var(--mfs-bg-hover);
        }

        .mint-flavor-switcher__option:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: -2px;
        }

        .mint-flavor-switcher__option[aria-current="page"] {
          background: var(--mfs-bg-current);
          color: var(--mfs-accent);
        }

        .mint-flavor-switcher__name {
          font-weight: 600;
        }

        .mint-flavor-switcher__description {
          color: var(--mfs-muted);
          font-size: 0.8125rem;
        }

        @media (prefers-reduced-motion: reduce) {
          .mint-flavor-switcher summary,
          .mint-flavor-switcher__chevron {
            transition: none;
          }
        }
      `}</style>

      <details>
        <summary aria-label={`${label}: ${selected.label}`}>
          <span className="mint-flavor-switcher__label">{selected.label}</span>
          <svg className="mint-flavor-switcher__chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="m6 9 6 6 6-6" />
          </svg>
        </summary>

        <ul className="mint-flavor-switcher__menu" aria-label={label}>
          {options.map(option => {
    const isCurrent = option.value === selected.value;
    const href = option.href ?? `${baseHref.replace(/\/$/, "")}/${encodeURIComponent(option.value)}`;
    return <li key={option.value}>
                <a className="mint-flavor-switcher__option" href={href} aria-current={isCurrent ? "page" : undefined}>
                  <span className="mint-flavor-switcher__name">
                    {option.label}
                  </span>
                  {option.description ? <span className="mint-flavor-switcher__description">
                      {option.description}
                    </span> : null}
                </a>
              </li>;
  })}
        </ul>
      </details>
    </div>;
};

<div className="mint-flavor-switcher-slot not-prose">
  <FlavorSwitcher
    current="js"
    baseHref="/doc/api-reference/widgets/geo-search"
    options={[
{ value: "js", label: "JavaScript", description: "InstantSearch.js" },
{ value: "react", label: "React", description: "React InstantSearch" },
]}
  />
</div>

```ts Signature theme={"system"}
geoSearch({
  container: string | HTMLElement,
  googleReference: object,
  // Optional parameters
  initialZoom?: number,
  initialPosition?: object,
  mapOptions?: object,
  builtInMarker?: object,
  customHTMLMarker?: object,
  enableRefine?: boolean,
  enableClearMapRefinement?: boolean,
  enableRefineControl?: boolean,
  enableRefineOnMapMove?: boolean,
  templates?: object,
  cssClasses?: object,
});
```

## Import

<CodeGroup>
  ```js Package manager theme={"system"}
  import { geoSearch } from 'instantsearch.js/es/widgets';
  ```

  ```js CDN theme={"system"}
  const { geoSearch } = instantsearch.widgets;
  // or directly use instantsearch.widgets.geoSearch()
  ```
</CodeGroup>

<Card title="See this widget in action" icon="monitor-play" href="https://instantsearchjs.netlify.app/stories/js/?path=/story/results-geosearch--default" horizontal>
  Preview this widget and its behavior.
</Card>

## About this widget

The `geoSearch` widget lets you search for results based on their position within a specified area (a bounding box).
It also provides features such as "search on map interactions".

<Note>
  The `geoSearch` widget doesn't let you [search around a central point](/doc/guides/managing-results/refine-results/geolocation/how-to/filter-results-around-a-location)
  or [within polygons](/doc/guides/managing-results/refine-results/geolocation/how-to/filter-results-inside-a-polygonal-area).
  If you want this, you need to build your own UI on top of the Algolia API.
</Note>

### Requirements

* Your hit records must have a [`_geoloc` attribute](/doc/guides/managing-results/refine-results/geolocation#enabling-geo-search-by-adding-geolocation-data-to-records) so they can be displayed on the map.
* You must load the [Google Maps library](https://developers.google.com/maps/documentation/javascript/overview) and pass a reference to the widget: the library doesn't come with InstantSearch.js.
* You must explicitly set the map container's `height`. For example:

```css CSS theme={"system"}
.ais-GeoSearch-map {
  height: 500px; // You can change this height 
}
```

## Examples

```js JavaScript icon=code theme={"system"}
geoSearch({
  container: "#geo-search",
  googleReference: window.google,
});
```

## Options

<ParamField body="container" type="string | HTMLElement" required>
  The CSS Selector or `HTMLElement` to insert the widget into.

  <CodeGroup>
    ```js string theme={"system"}
    geoSearch({
      // ...
      container: "#geo-search",
    });
    ```

    ```js HTMLElement theme={"system"}
    geoSearch({
      // ...
      container: document.querySelector("#geo-search"),
    });
    ```
  </CodeGroup>
</ParamField>

<ParamField body="googleReference" type="object" required>
  The reference to the global `window.google` object.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    googleReference: window.google,
  });
  ```
</ParamField>

<ParamField body="initialZoom" type="number" default={1}>
  By default, the map sets the zoom level based on the displayed markers (results).
  However, after InstantSearch has applied a refinement, there may be no results.
  When this happens, a specific zoom level is required to render the map.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    initialZoom: 4,
  });
  ```
</ParamField>

<ParamField body="initialPosition" type="object" default="{lat: 0, lng: 0}">
  By default, the map sets the position based on the displayed markers (results).
  However, after InstantSearch has applied a refinement, there may be no results.
  When this happens, a specific position is required to render the map.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    initialPosition: {
      lat: 48.864716,
      lng: 2.349014,
    },
  });
  ```
</ParamField>

<ParamField body="mapOptions" type="object">
  The [options forwarded to the Google Maps constructor](https://developers.google.com/maps/documentation/javascript/reference#MapOptions).

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    mapOptions: {
      streetViewControl: true,
    },
  });
  ```
</ParamField>

<ParamField body="builtInMarker" type="object">
  The options for customizing the built-in Google Maps markers.
  This is ignored when the [`customHTMLMarker`](#param-custom-html-marker) is provided.
  The object accepts multiple attributes:

  * `createOptions: function`. A function to create the [options passed to the Google Maps marker](https://developers.google.com/maps/documentation/javascript/reference#MapOptions). The function is called with `item`, which is the `hit` tied to the marker.
  * `events: object`. An object that takes event types (such as `click` and `mouseover`) as keys and listeners as values. The listener is called with an object that contains properties `event`, `item`, `marker`, and `map`.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    builtInMarker: {
      createOptions(item) {
        return {
          title: item.name,
        };
      },
      events: {
        click({ event, item, marker, map }) {
          console.log(item);
        },
      },
    },
  });
  ```
</ParamField>

<ParamField body="customHTMLMarker" type="object">
  The options for customizing the HTML marker.
  InstantSearch.js provides an alternative to the built-in Google Maps markers.
  You can use plain HTML to build your custom markers (see [`templates.HTMLMarker`](/doc/api-reference/widgets/geo-search/js#param-html-marker)).
  The `customHTMLMarker` object accepts several attributes:

  * `createOptions: function`. A function to create the options passed to the `HTMLMarker`. The only supported option is `anchor`. It lets you shift the marker position from the center of the element.
  * `events: object`. An object that takes event types (such as `click` and `mouseover`) as keys and listeners as values. The listener is called with an object that contains properties `event`, `item`, `marker`, and `map`.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    customHTMLMarker: {
      createOptions(item) {
        return {
          anchor: {
            x: 0,
            y: 0,
          },
        };
      },
      events: {
        click({ event, item, marker, map }) {
          console.log(item);
        },
      },
    },
  });
  ```
</ParamField>

<ParamField body="enableRefine" type="boolean" default={true}>
  If `true`, the map is used for refining the search.
  Otherwise, it's only for display purposes.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    enableRefine: false,
  });
  ```
</ParamField>

<ParamField body="enableClearMapRefinement" type="boolean" default={true}>
  If `true`, a button is displayed on the map to allow users to clear any refinements.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    enableClearMapRefinement: false,
  });
  ```
</ParamField>

<ParamField body="enableRefineControl" type="boolean" default={true}>
  If `true`, users can toggle the option [`enableRefineOnMapMove`](/doc/api-reference/widgets/geo-search/js#param-enable-refine-on-map-move) directly from the map.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    enableRefineControl: false,
  });
  ```
</ParamField>

<ParamField body="enableRefineOnMapMove" type="boolean" default={true}>
  If `true`, refine is triggered as you move the map.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    enableRefineOnMapMove: false,
  });
  ```
</ParamField>

<ParamField body="templates" type="object">
  The [templates](#templates) to use for the widget.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    templates: {
      // ...
    },
  });
  ```
</ParamField>

<ParamField body="cssClasses" type="object" default="{}">
  The [CSS classes you can override](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#style-your-widgets):

  * `root`. The widget's root element.
  * `map`. The map element.
  * `control`. The control element.
  * `label`. The label for the control element.
  * `selectedLabel`. The control element's selected label.
  * `input`. The control element's input.
  * `redo`. The "Redo search" button.
  * `disabledRedo`. The disabled "Redo search" button.
  * `reset`. The "Reset refinement" button.

  ```js JavaScript icon=code theme={"system"}
  geoSearch({
    // ...
    cssClasses: {
      root: "MyCustomGeoSearch",
      map: ["MyCustomGeoSearchMap", "MyCustomGeoSearchMap--subclass"],
    },
  });
  ```
</ParamField>

## Templates

You can customize parts of a widget’s UI using the Templates API.

Each template includes an `html` function,
which you can use as a [tagged template](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates).
This function safely renders templates as HTML strings and works directly in the browser—no build step required.
For details, see [Templating your UI](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js/#templating-your-ui).

<Note>
  The `html` function is available in InstantSearch.js version 4.46.0 or later.
</Note>

<ParamField body="HTMLMarker" type="string | function">
  The template to use for the marker.

  <CodeGroup>
    ```js function theme={"system"}
    geoSearch({
      // ...
      templates: {
        HTMLMarker(_, { html }) {
          return html`<p>Your custom HTML Marker</p>`;
        },
      },
    });
    ```

    ```js string theme={"system"}
    // String-based templates are deprecated, use functions instead.
    geoSearch({
      // ...
      templates: {
        HTMLMarker: "<p>Your custom HTML Marker</p>",
      },
    });
    ```
  </CodeGroup>
</ParamField>

<ParamField body="reset" type="string | function">
  The template for the reset button.

  <CodeGroup>
    ```js function theme={"system"}
    geoSearch({
      // ...
      templates: {
        reset(_, { html }) {
          return html`<strong>Clear the map refinement</strong>`;
        },
      },
    });
    ```

    ```js string theme={"system"}
    // String-based templates are deprecated, use functions instead.
    geoSearch({
      // ...
      templates: {
        reset: "Clear the map refinement",
      },
    });
    ```
  </CodeGroup>
</ParamField>

<ParamField body="toggle" type="string | function">
  The template for the toggle label.

  <CodeGroup>
    ```js function theme={"system"}
    geoSearch({
      // ...
      templates: {
        toggle(_, { html }) {
          return html`<strong>Search as I move the map</strong>`;
        },
      },
    });
    ```

    ```js string theme={"system"}
    // String-based templates are deprecated, use functions instead.
    geoSearch({
      // ...
      templates: {
        toggle: "Search as I move the map",
      },
    });
    ```
  </CodeGroup>
</ParamField>

<ParamField body="redo" type="string | function">
  The template for the redo label.

  <CodeGroup>
    ```js function theme={"system"}
    geoSearch({
      // ...
      templates: {
        redo(_, { html }) {
          return html`<strong>Redo search here</strong>`;
        },
      },
    });
    ```

    ```js string theme={"system"}
    // String-based templates are deprecated, use functions instead.
    geoSearch({
      // ...
      templates: {
        redo: "Redo search here",
      },
    });
    ```
  </CodeGroup>
</ParamField>

## HTML output

```html HTML icon=code-xml theme={"system"}
<div class="ais-GeoSearch">
  <div class="ais-GeoSearch-map">
    <!-- Map element here -->
  </div>
  <div class="ais-GeoSearch-control">
    <label class="ais-GeoSearch-label">
      <input class="ais-GeoSearch-input" type="checkbox" />
      Search as I move the map
    </label>
  </div>
  <button class="ais-GeoSearch-reset">Clear the map refinement</button>
</div>
```

## Customize the UI with `connectGeoSearch`

If you want to create your own UI of the `geoSearch` widget, you can use connectors.

To use `connectGeoSearch`, you can import it with the declaration relevant to how you installed InstantSearch.js.

<CodeGroup>
  ```js Package manager theme={"system"}
  import { connectGeoSearch } from 'instantsearch.js/es/connectors';
  ```

  ```js CDN theme={"system"}
  const { connectGeoSearch } = instantsearch.connectors;
  // or directly use instantsearch.connectors.connectGeoSearch()
  ```
</CodeGroup>

Then it's a 3-step process:

```js JavaScript icon=code theme={"system"}
// 1. Create a render function
const renderGeoSearch = (renderOptions, isFirstRender) => {
  // Rendering logic
};

// 2. Create the custom widget
const customGeoSearch = connectGeoSearch(renderGeoSearch);

// 3. Instantiate
search.addWidgets([
  customGeoSearch({
    // instance params
  }),
]);
```

### Create a render function

This rendering function is called before the first search (`init` lifecycle step)
and each time results come back from Algolia (`render` lifecycle step).

```js JavaScript icon=code theme={"system"}
const renderGeoSearch = (renderOptions, isFirstRender) => {
  const {
    items,
    position,
    currentRefinement,
    refine,
    sendEvent,
    clearMapRefinement,
    isRefinedWithMap,
    toggleRefineOnMapMove,
    isRefineOnMapMove,
    setMapMoveSinceLastRefine,
    hasMapMoveSinceLastRefine,
    widgetParams,
  } = renderOptions;

  if (isFirstRender) {
    // Do some initial rendering and bind events
  }

  // Render the widget
};
```

#### Rendering options

The following rendering option example code snippets use [Leaflet](https://leafletjs.com/examples/quick-start) to render the map.
If you prefer not to use Leaflet, you can use another library (such as [Google Maps](https://developers.google.com/maps/documentation/javascript/overview) or [Mapbox](https://www.mapbox.com)).

<ParamField body="items" type="object[]">
  Hits that matched the search request.

  ```js JavaScript icon=code theme={"system"}
  let 
  let markers = [];

  const renderGeoSearch = (renderOptions, isFirstRender) => {
    const { items } = renderOptions;

    if (isFirstRender) {
      map = L.map(document.querySelector("#geo-search"));

      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution:
          '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
      }).addTo(map);
    }

    markers.forEach((marker) => marker.remove());

    markers = items.map(({ _geoloc }) =>
      L.marker([_geoloc.lat, _geoloc.lng]).addTo(map),
    );

    if (markers.length) {
      map.fitBounds(L.featureGroup(markers).getBounds());
    }
  };
  ```
</ParamField>

<ParamField body="position" type="object">
  The current search position, when applicable.

  ```js JavaScript icon=code theme={"system"}
  let map = null;
  let markers = [];

  const renderGeoSearch = (renderOptions, isFirstRender) => {
    const { items, position } = renderOptions;

    if (isFirstRender) {
      map = L.map(document.querySelector("#geo-search"));

      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution:
          '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
      }).addTo(map);
    }

    markers.forEach((marker) => marker.remove());

    markers = items.map(({ _geoloc }) =>
      L.marker([_geoloc.lat, _geoloc.lng]).addTo(map),
    );

    if (markers.length) {
      map.fitBounds(L.featureGroup(markers).getBounds());
    } else {
      map.setView(
        position || {
          lat: 48.864716,
          lng: 2.349014,
        },
        12,
      );
    }
  };
  ```
</ParamField>

<ParamField body="currentRefinement" type="object">
  The search's bounding box:

  * `northEast: { lat: number, lng: number }`. The top right corner of the map view.
  * `southWest: { lat: number, lng: number }`. The bottom left corner of the map view.
</ParamField>

<ParamField body="refine" type="function">
  Sets a bounding box to filter the results from the given map bounds. The function accepts an object with:

  * `northEast: { lat: number, lng: number }`. The top right corner of the map view.
  * `southWest: { lat: number, lng: number }`. The bottom left corner of the map view.
</ParamField>

<ParamField body="sendEvent" type="(eventType, hit, eventName) => void">
  The function to send `click` or `conversion` events.
  The `view` event is automatically sent when this connector renders hits.
  To learn more, see the [`insights`](/doc/api-reference/widgets/insights/js) middleware.

  * `eventType: 'click' | 'conversion'`
  * `hit: Hit | Hit[]`
  * `eventName: string`

  ```js JavaScript icon=code theme={"system"}
  // For example,
  sendEvent("click", hit, "Location Starred");
  // or
  sendEvent("conversion", hit, "Restaurant Saved");

  /*
    A payload like the following will be sent to the `insights` middleware.
    {
      eventType: 'click',
      insightsMethod: 'clickedObjectIDsAfterSearch',
      payload: {
        eventName: 'Product Added',
        index: '<index-name>',
        objectIDs: ['<object-id>'],
        positions: [<position>],
        queryID: '<query-id>',
      },
      widgetType: 'ais.geoSearch',
    }
  */
  ```
</ParamField>

<ParamField body="clearMapRefinement" type="function">
  Resets the current bounding box refinement.
</ParamField>

<ParamField body="isRefinedWithMap" type="function">
  Returns `true` if the current refinement is set with the map bounds.
</ParamField>

<ParamField body="toggleRefineOnMapMove" type="function">
  Toggles whether users can refine on map move.
</ParamField>

<ParamField body="isRefineOnMapMove" type="function">
  Returns `true` if users can refine on map move.
</ParamField>

<ParamField body="setMapMoveSinceLastRefine" type="function">
  Indicates that the map has moved since the last refinement.
  This function should be called on each map move.
  It helps ensure the map is only re-rendered when it has moved.
</ParamField>

<ParamField body="hasMapMoveSinceLastRefine" type="function">
  Returns `true` if the map has moved since the last refinement.
</ParamField>

<ParamField body="widgetParams" type="object">
  All original widget options forwarded to the render function.
</ParamField>

### Create and instantiate the custom widget

First, create your custom widgets using a rendering function.
Then, instantiate them with parameters.

There are two kinds of parameters you can pass:

* **Instance parameters**. Predefined options that configure Algolia's behavior.
* **Custom parameters**. Parameters you define to make the widget reusable and adaptable.

Inside the `renderFunction`, both instance and custom parameters are accessible through `connector.widgetParams`.

```js JavaScript icon=code theme={"system"}
const customGeoSearch = connectGeoSearch(renderGeoSearch);

search.addWidgets([
  customGeoSearch({
    // Optional parameters
    enableRefineOnMapMove,
    transformItems,
  }),
]);
```

#### Instance options

<ParamField body="enableRefineOnMapMove" type="boolean" default={true}>
  If `true`, refine is triggered as you move the map.

  ```js JavaScript icon=code theme={"system"}
  customGeoSearch({
    enableRefineOnMapMove: false,
  });
  ```
</ParamField>

<ParamField body="transformItems" type="function" default="items => items">
  A function that receives the list of items before they are displayed.
  It should return a new array with the same structure.
  Use this to transform, filter, or reorder the items.

  The function also has access to the full `results` data,
  including all standard [response parameters](/doc/guides/building-search-ui/going-further/backend-search/in-depth/understanding-the-api-response/)
  and [parameters from the helper](https://community.algolia.com/algoliasearch-helper-js/reference.html#query-parameters),
  such as `disjunctiveFacetsRefinements`.

  ```js JavaScript icon=code theme={"system"}
  customGeoSearch({
    transformItems(items) {
      return items.map((item) => ({
        ...item,
        name: item.name.toUpperCase(),
      }));
    },
  });

  // or, combined with results 
  customGeoSearch({
    transformItems(items, { results }) {
      return items.query.length === 0
        ? items
        : items.map((item) => ({
            ...item,
            name: `${item.name} (matching "${results.query}")`,
          }));
    },
  });
  ```
</ParamField>

### Full example

<CodeGroup>
  ```html HTML theme={"system"}
  <div id="geo-search"></div>
  ```

  ```js JavaScript theme={"system"}
  // Create the render function
  let map = null;
  let markers = [];
  let isUserInteraction = true;

  const renderGeoSearch = (renderOptions, isFirstRendering) => {
    const { items, currentRefinement, refine, clearMapRefinement, widgetParams } =
      renderOptions;

    const { initialZoom, initialPosition, container } = widgetParams;

    if (isFirstRendering) {
      const element = document.createElement("div");
      element.style.height = "100%";

      const button = document.createElement("button");
      button.textContent = "Clear the map refinement";

      map = L.map(element);

      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution:
          '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
      }).addTo(map);

      map.on("moveend", () => {
        if (isUserInteraction) {
          const ne = map.getBounds().getNorthEast();
          const sw = map.getBounds().getSouthWest();

          refine({
            northEast: { lat: ne.lat, lng: ne.lng },
            southWest: { lat: sw.lat, lng: sw.lng },
          });
        }
      });

      button.addEventListener("click", () => {
        clearMapRefinement();
      });

      container.appendChild(element);
      container.appendChild(button);
    }

    container.querySelector("button").hidden = !currentRefinement;

    markers.forEach((marker) => marker.remove());

    markers = items.map(({ _geoloc }) =>
      L.marker([_geoloc.lat, _geoloc.lng]).addTo(map),
    );

    isUserInteraction = false;
    if (!currentRefinement && markers.length) {
      map.fitBounds(L.featureGroup(markers).getBounds(), {
        animate: false,
      });
    } else if (!currentRefinement) {
      map.setView(initialPosition, initialZoom, {
        animate: false,
      });
    }
    isUserInteraction = true;
  };

  // Create the custom widget
  const customGeoSearch = connectGeoSearch(renderGeoSearch);

  // Instantiate the custom widget
  search.addWidgets([
    customGeoSearch({
      container: document.querySelector("#geo-search"),
      initialZoom: 12,
      initialPosition: {
        lat: 48.864716,
        lng: 2.349014,
      },
    }),
  ]);
  ```
</CodeGroup>
