> Complete content index: https://narsil.sondelali.com/llms.txt

# Geosearch

> Index coordinates as geopoints and filter by radius or polygon alongside a search term and every other query feature.

URL: https://narsil.sondelali.com/docs/geosearch
Section: Search modes
Version: 0.2 (@delali/narsil@0.2.2, latest)

A `geopoint` field holds a `{ lat, lon }` pair, and geo conditions are ordinary filters, so they compose with a term, other filters, facets, and every other query feature. A filter refines a search rather than running on its own, so each query below pairs the term `market` with a location filter. This page builds one file, `stores.ts`, step by step. You index a handful of markets around Toronto, find the ones within a radius, then switch that radius for a polygon.

## Index locations

Start `stores.ts` with an engine, a `stores` index that declares a `geopoint` field, and a few markets with real coordinates around Toronto.

```ts title="stores.ts"

const narsil = await createNarsil()

await narsil.createIndex('stores', {
  schema: {
    name: 'string',
    location: 'geopoint',
  },
})

await narsil.insertBatch('stores', [
  { id: 'st-lawrence', name: 'St. Lawrence Market', location: { lat: 43.6487, lon: -79.3716 } },
  { id: 'kensington', name: 'Kensington Market', location: { lat: 43.6547, lon: -79.4005 } },
  { id: 'markham', name: 'Markham Farmers Market', location: { lat: 43.8561, lon: -79.337 } },
  { id: 'mississauga', name: 'Mississauga Chinatown Market', location: { lat: 43.589, lon: -79.6441 } },
])
```

## Search by radius

Add a query that searches for `market` and keeps every result within five kilometres of a point in downtown Toronto. The `radius` filter takes a centre, a `distance`, and a `unit`. The print rounds the scores to two places but leaves `lat` and `lon` untouched, so the coordinates stay exact.

```ts title="stores.ts"

const narsil = await createNarsil()

await narsil.createIndex('stores', {
  schema: {
    name: 'string',
    location: 'geopoint',
  },
})

await narsil.insertBatch('stores', [
  { id: 'st-lawrence', name: 'St. Lawrence Market', location: { lat: 43.6487, lon: -79.3716 } },
  { id: 'kensington', name: 'Kensington Market', location: { lat: 43.6547, lon: -79.4005 } },
  { id: 'markham', name: 'Markham Farmers Market', location: { lat: 43.8561, lon: -79.337 } },
  { id: 'mississauga', name: 'Mississauga Chinatown Market', location: { lat: 43.589, lon: -79.6441 } },
])

const nearby = await narsil.query('stores', { // [!code ++:12]
  term: 'market',
  filters: {
    fields: {
      location: {
        radius: { lat: 43.652, lon: -79.385, distance: 5, unit: 'km' },
      },
    },
  },
})

console.log(JSON.stringify(nearby, (k, v) => (typeof v === 'number' && k !== 'lat' && k !== 'lon' ? Number(v.toFixed(2)) : v), 2))
```

```json result
{
  "hits": [
    {
      "id": "kensington",
      "score": 0.12,
      "document": {
        "id": "kensington",
        "name": "Kensington Market",
        "location": {
          "lat": 43.6547,
          "lon": -79.4005
        }
      }
    },
    {
      "id": "st-lawrence",
      "score": 0.1,
      "document": {
        "id": "st-lawrence",
        "name": "St. Lawrence Market",
        "location": {
          "lat": 43.6487,
          "lon": -79.3716
        }
      }
    }
  ],
  "count": 2,
  "elapsed": 0.28
}
```

The two downtown markets fall inside the circle, and both score on the term `market` in their names. Markham and Mississauga are well beyond five kilometres, so they drop out. Radius filters measure <Tooltip tip="The Haversine formula computes the distance between two points over the surface of a sphere. It treats the Earth as a perfect sphere, which is accurate to within about half a per cent.">Haversine</Tooltip> distance by default and accept `unit: 'km' | 'mi' | 'm'`. Set `highPrecision: true` to switch to <Tooltip tip="Vincenty's formula models the Earth as a flattened ellipsoid and iterates towards the answer, which gives millimetre-level accuracy over long distances.">Vincenty's iterative formula</Tooltip> for long-distance accuracy.

## Search by polygon

A radius covers a circle, but a delivery zone or a district boundary is rarely a neat circle. Swap the `radius` filter for a `polygon` filter that lists the corners of the area you care about. The rest of the query stays the same.

```ts title="stores.ts"

const narsil = await createNarsil()

await narsil.createIndex('stores', {
  schema: {
    name: 'string',
    location: 'geopoint',
  },
})

await narsil.insertBatch('stores', [
  { id: 'st-lawrence', name: 'St. Lawrence Market', location: { lat: 43.6487, lon: -79.3716 } },
  { id: 'kensington', name: 'Kensington Market', location: { lat: 43.6547, lon: -79.4005 } },
  { id: 'markham', name: 'Markham Farmers Market', location: { lat: 43.8561, lon: -79.337 } },
  { id: 'mississauga', name: 'Mississauga Chinatown Market', location: { lat: 43.589, lon: -79.6441 } },
])

const nearby = await narsil.query('stores', {
  term: 'market',
  filters: {
    fields: {
      location: {
        radius: { lat: 43.652, lon: -79.385, distance: 5, unit: 'km' }, // [!code --]
        polygon: { // [!code ++:8]
          points: [
            { lat: 43.63, lon: -79.42 },
            { lat: 43.63, lon: -79.36 },
            { lat: 43.68, lon: -79.36 },
            { lat: 43.68, lon: -79.42 },
          ],
        },
      },
    },
  },
})

console.log(JSON.stringify(nearby, (k, v) => (typeof v === 'number' && k !== 'lat' && k !== 'lon' ? Number(v.toFixed(2)) : v), 2))
```

```json result
{
  "hits": [
    {
      "id": "kensington",
      "score": 0.12,
      "document": {
        "id": "kensington",
        "name": "Kensington Market",
        "location": {
          "lat": 43.6547,
          "lon": -79.4005
        }
      }
    },
    {
      "id": "st-lawrence",
      "score": 0.1,
      "document": {
        "id": "st-lawrence",
        "name": "St. Lawrence Market",
        "location": {
          "lat": 43.6487,
          "lon": -79.3716
        }
      }
    }
  ],
  "count": 2,
  "elapsed": 0.12
}
```

Polygon filters test containment with <Tooltip tip="Ray casting tests whether a point is inside a polygon by drawing a line from the point outwards and counting how many times it crosses the boundary. An odd count means the point is inside.">ray casting</Tooltip>, so the points describe the boundary in order and the filter returns every market inside it. The downtown box holds the same two markets the radius did.

## Invert the match

Both filter shapes accept `inside: false` to flip the result and return the documents outside the area instead. This variation stands on its own, so it gets its own file, `outside.ts`, built on the same `stores` index and the same centre point.

```ts title="outside.ts"

const narsil = await createNarsil()

await narsil.createIndex('stores', {
  schema: {
    name: 'string',
    location: 'geopoint',
  },
})

await narsil.insertBatch('stores', [
  { id: 'st-lawrence', name: 'St. Lawrence Market', location: { lat: 43.6487, lon: -79.3716 } },
  { id: 'kensington', name: 'Kensington Market', location: { lat: 43.6547, lon: -79.4005 } },
  { id: 'markham', name: 'Markham Farmers Market', location: { lat: 43.8561, lon: -79.337 } },
  { id: 'mississauga', name: 'Mississauga Chinatown Market', location: { lat: 43.589, lon: -79.6441 } },
])

const outside = await narsil.query('stores', {
  term: 'market',
  filters: {
    fields: {
      location: {
        radius: { lat: 43.652, lon: -79.385, distance: 5, unit: 'km', inside: false },
      },
    },
  },
})

console.log(JSON.stringify(outside, (k, v) => (typeof v === 'number' && k !== 'lat' && k !== 'lon' ? Number(v.toFixed(2)) : v), 2))
```

```json result
{
  "hits": [
    {
      "id": "markham",
      "score": 0.1,
      "document": {
        "id": "markham",
        "name": "Markham Farmers Market",
        "location": {
          "lat": 43.8561,
          "lon": -79.337
        }
      }
    },
    {
      "id": "mississauga",
      "score": 0.1,
      "document": {
        "id": "mississauga",
        "name": "Mississauga Chinatown Market",
        "location": {
          "lat": 43.589,
          "lon": -79.6441
        }
      }
    }
  ],
  "count": 2,
  "elapsed": 0.07
}
```

Flipping the radius returns the two markets outside the circle, Markham to the north and Mississauga to the west, in place of the downtown pair.
