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

# Geo search

> Learn how to use geographical search in your React InstantSearch app.

export const Records = () => <Tooltip tip="A record is a searchable object in an Algolia index. Each record consists of named attributes." cta="Algolia records" href="/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records">
    records
  </Tooltip>;

export const Index = () => <Tooltip tip="An Algolia index is a searchable dataset that consists of records and configuration settings. These settings define how the records are searched and ranked.">
    index
  </Tooltip>;

<div className="not-prose algolia-flavor-switcher">
  <div className="afs-dropdown">
    <div className="afs-trigger" role="button" tabIndex="0" aria-haspopup="listbox">
      <span className="afs-current">React</span>

      <svg className="afs-chevron lucide lucide-chevron-down" width="24" height="24" 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>
    </div>

    <ul className="afs-menu" role="listbox">
      <li role="option" aria-selected="false"><a className="afs-option" href="/doc/guides/building-search-ui/ui-and-ux-patterns/geo-search/js"><span className="afs-option-name">JavaScript</span><span className="afs-option-lib">InstantSearch.js</span></a></li>
      <li role="option" aria-selected="true"><a className="afs-option is-current" href="/doc/guides/building-search-ui/ui-and-ux-patterns/geo-search/react"><span className="afs-option-name">React</span><span className="afs-option-lib">React InstantSearch</span></a></li>
      <li role="option" aria-selected="false"><a className="afs-option" href="/doc/guides/building-search-ui/ui-and-ux-patterns/geo-search/vue"><span className="afs-option-name">Vue</span><span className="afs-option-lib">Vue InstantSearch</span></a></li>
      <li role="option" aria-selected="false"><a className="afs-option" href="/doc/guides/building-search-ui/ui-and-ux-patterns/geo-search/ios"><span className="afs-option-name">iOS</span><span className="afs-option-lib">InstantSearch iOS</span></a></li>
      <li role="option" aria-selected="false"><a className="afs-option" href="/doc/guides/building-search-ui/ui-and-ux-patterns/geo-search/android"><span className="afs-option-name">Android</span><span className="afs-option-lib">InstantSearch Android</span></a></li>
    </ul>
  </div>
</div>

<Note>
  This is the **React InstantSearch v7** documentation.
  If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react/#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
  If you were using React InstantSearch Hooks,
  this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react/#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
  To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
</Note>

You can use the [geographical search capabilities](/doc/guides/managing-results/refine-results/geolocation#geo-search) of Algolia with React InstantSearch by creating a custom widget based on the [`useGeoSearch()`](/doc/api-reference/widgets/geo-search/react) Hook.
This Hook isn't tied to any map provider, so you can select and implement any solution.

This guide uses [Leaflet](https://leafletjs.com/) as the map provider through its React wrapper, [React Leaflet](https://react-leaflet.js.org/).
To learn more about loading and using leaflet, visit the [Leaflet documentation](https://leafletjs.com/examples/quick-start/).

<Columns>
  <Card title="Open CodeSandbox" icon="codesandbox" href="https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/react-instantsearch/geo-search?file=/src/App.tsx">
    Run and edit the Geo search example in CodeSandbox.
  </Card>

  <Card title="Explore source code" icon="github" href="https://github.com/algolia/doc-code-samples/tree/master/react-instantsearch/geo-search">
    Browse the source for the Geo search example on GitHub.
  </Card>
</Columns>

## Dataset

This guide use a dataset of over 3,000 <Records /> of the biggest airports in the world.

```json JSON icon=braces theme={"system"}
{
  "objectID": "3797",
  "name": "John F Kennedy Intl",
  "city": "New York",
  "country": "United States",
  "iata_code": "JFK",
  "links_count": 911,
  "_geoloc": {
    "lat": 40.639751,
    "lng": -73.778925
  }
}
```

Latitude and longitude are stored in the record to allow hits to be displayed on the map.
You should store them in the `_geoloc` attribute to enable geo-filtering and geo-sorting.

You can download the dataset on [GitHub](https://github.com/algolia/datasets/tree/master/airports).
Have a look at how to import it in [Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-from-the-dashboard).

### Configure index settings

When displaying on a map, you still want the relevance to be good.
For that, configure the <Index /> as follows:

* [Searchable attributes](/doc/guides/managing-results/must-do/searchable-attributes) should be set to enable search in the four textual attributes: `name`, `city`, `country`, and `iata_code`.
* [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking): use the **number of other connected airports** `links_count` as a ranking metric. The more connections the better.

<CodeGroup>
  ```cs C# theme={"system"}
  var response = await client.SetSettingsAsync(
    "INDEX_NAME",
    new IndexSettings
    {
      SearchableAttributes = new List<string> { "name", "country", "city", "iata_code" },
      CustomRanking = new List<string> { "desc(links_count)" },
    }
  );
  ```

  ```dart Dart theme={"system"}
  final response = await client.setSettings(
    indexName: "INDEX_NAME",
    indexSettings: IndexSettings(
      searchableAttributes: [
        "name",
        "country",
        "city",
        "iata_code",
      ],
      customRanking: [
        "desc(links_count)",
      ],
    ),
  );
  ```

  ```go Go theme={"system"}
  response, err := client.SetSettings(client.NewApiSetSettingsRequest(
    "INDEX_NAME",
    search.NewEmptyIndexSettings().SetSearchableAttributes(
      []string{"name", "country", "city", "iata_code"}).SetCustomRanking(
      []string{"desc(links_count)"})))
  if err != nil {
    // handle the eventual error
    panic(err)
  }
  ```

  ```java Java theme={"system"}
  UpdatedAtResponse response = client.setSettings(
    "INDEX_NAME",
    new IndexSettings()
      .setSearchableAttributes(Arrays.asList("name", "country", "city", "iata_code"))
      .setCustomRanking(Arrays.asList("desc(links_count)"))
  );
  ```

  ```js JavaScript theme={"system"}
  const response = await client.setSettings({
    indexName: 'theIndexName',
    indexSettings: {
      searchableAttributes: ['name', 'country', 'city', 'iata_code'],
      customRanking: ['desc(links_count)'],
    },
  });
  ```

  ```kotlin Kotlin theme={"system"}
  var response =
    client.setSettings(
      indexName = "INDEX_NAME",
      indexSettings =
        IndexSettings(
          searchableAttributes = listOf("name", "country", "city", "iata_code"),
          customRanking = listOf("desc(links_count)"),
        ),
    )
  ```

  ```php PHP theme={"system"}
  $response = $client->setSettings(
      'INDEX_NAME',
      ['searchableAttributes' => [
          'name',

          'country',

          'city',

          'iata_code',
      ],
          'customRanking' => [
              'desc(links_count)',
          ],
      ],
  );
  ```

  ```python Python theme={"system"}
  response = client.set_settings(
      index_name="INDEX_NAME",
      index_settings={
          "searchableAttributes": [
              "name",
              "country",
              "city",
              "iata_code",
          ],
          "customRanking": [
              "desc(links_count)",
          ],
      },
  )
  ```

  ```ruby Ruby theme={"system"}
  response = client.set_settings(
    "INDEX_NAME",
    Algolia::Search::IndexSettings.new(
      searchable_attributes: ["name", "country", "city", "iata_code"],
      custom_ranking: ["desc(links_count)"]
    )
  )
  ```

  ```scala Scala theme={"system"}
  val response = Await.result(
    client.setSettings(
      indexName = "INDEX_NAME",
      indexSettings = IndexSettings(
        searchableAttributes = Some(Seq("name", "country", "city", "iata_code")),
        customRanking = Some(Seq("desc(links_count)"))
      )
    ),
    Duration(100, "sec")
  )
  ```

  ```swift Swift theme={"system"}
  let response = try await client.setSettings(
      indexName: "INDEX_NAME",
      indexSettings: IndexSettings(
          searchableAttributes: ["name", "country", "city", "iata_code"],
          customRanking: ["desc(links_count)"]
      )
  )
  ```
</CodeGroup>

## Display hits on the map

<Steps>
  <Step title="Add the Leaflet map container">
    By default, the map container has height 0, so make sure to set an explicit height, for example, using CSS.

    ```tsx React icon=code theme={"system"}
    // App.tsx
    import React from "react";
    import { liteClient as algoliasearch } from "algoliasearch/lite";
    import { InstantSearch, SearchBox } from "react-instantsearch";
    import { MapContainer, TileLayer } from "react-leaflet";

    const searchClient = algoliasearch(
      "latency",
      "6be0576ff61c053d5f9a3225e2a90f76",
    );

    export function App() {
      return (
        <InstantSearch searchClient={searchClient} indexName="airports">
          <SearchBox placeholder="Search for airports..." />
          <MapContainer
            style={{ height: "500px" }}
            center={[48.85, 2.35]}
            zoom={10}
          >
            <TileLayer
              attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
              url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
            />
          </MapContainer>
        </InstantSearch>
      );
    }
    ```
  </Step>

  <Step title="Create custom widget">
    To populate the map with markers, create a [custom widget](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react) for React InstantSearch, using the [`useGeoSearch()`](/doc/api-reference/widgets/geo-search/react) Hook you created before.

    ```tsx React icon=code theme={"system"}
    // Airports.tsx
    import React from "react";
    import { Marker, Popup } from "react-leaflet";
    import { useGeoSearch } from "react-instantsearch";

    type Airport = {
      name: string;
      city: string;
      country: string;
      iata_code: string;
      links_count: number;
    };

    export function Airports() {
      const { items } = useGeoSearch<Airport>();

      return (
        <>
          {items.map((item) => (
            <Marker key={item.objectID} position={item._geoloc}>
              <Popup>
                <strong>{item.name}</strong>
                <br />
                {item.city}, {item.country}
              </Popup>
            </Marker>
          ))}
        </>
      );
    }
    ```
  </Step>

  <Step title="Import custom widget">
    Import the widget and add it inside `MapContainer`.
    You should now see markers representing the airports locations.
  </Step>
</Steps>

## Move the map

To add some interactivity, you can detect users interactions and update the list of airports to match the new viewable area of the map.

```tsx React icon=code theme={"system"}
// ...
import { Marker, Popup, useMapEvents } from 'react-leaflet';

export function Airports() {
  const {
    items,
    refine: refineItems,
  } = useGeoSearch();

  const onViewChange = ({ target }) => {
    refineItems({
      northEast: target.getBounds().getNorthEast(),
      southWest: target.getBounds().getSouthWest(),
    });
  };

  const map = useMapEvents({
    zoomend: onViewChange,
    dragend: onViewChange,
  });

  return (
    /* ... */
  )
}
```

## React to search query updates

In the current form, if you type a query in the search box, for example, "Italy", the markers disappear.
That's because the list of markers now show all the airports in Italy, but the map is still in its initial location.

To move the map when the search query changes,
use the [`useSearchBox`](/doc/api-reference/widgets/search-box/react#hook) Hook to retrieve the current query,
and move the map programmaticaly when it changes.

To not interfere with the manual movement of the map, you need to clear the boundaries refinement when a query changes.
In the `onViewChange` event handler, you also need to reset the query and set a flag that instructs the application to not move the map programmatically in this case.

```tsx React icon=code theme={"system"}
// ...
import { useState } from 'react';
import { useSearchBox } from 'react-instantsearch';

export function Airports() {
  const { query, refine: refineQuery } = useSearchBox();
  const {
    items,
    refine: refineItems,
    currentRefinement,
    clearMapRefinement,
  } = useGeoSearch();

  const [previousQuery, setPreviousQuery] = useState(query);
  const [skipViewEffect, setSkipViewEffect] = useState(false);

  // When users move the map, clear the query if necessary to only
  // refine on the new boundaries of the map.
  const onViewChange = ({ target }) => {
    setSkipViewEffect(true);

    if (query.length > 0) {
      refineQuery('');
    }

    refineItems({
      northEast: target.getBounds().getNorthEast(),
      southWest: target.getBounds().getSouthWest(),
    });
  };

  const map = useMapEvents({
    zoomend: onViewChange,
    dragend: onViewChange,
  });

  // When the query changes, remove the boundary refinement if necessary and
  // center the map on the first result.
  if (query !== previousQuery) {
    if (currentRefinement) {
      clearMapRefinement();
    }

    // `skipViewEffect` allows us to bail out of centering on the first result
    // if the query has been cleared programmatically.
    if (items.length > 0 && !skipViewEffect) {
      map.setView(items[0]._geoloc);
    }

    setSkipViewEffect(false);
    setPreviousQuery(query);
  }

  return (
    /* ... */
  );
}
```
