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

# Geo

> Find the places among the entities of a text and store their coordinates.

Some entities are places: a city, a country, a region, a landmark. `remember` finds them and stores their coordinates, so the
graph knows where things are. The code is the package `internal/geo` (the resolvers) and `memory_remember/remember_geo.go` (the
stage of the pipeline); the data is the table `gb_entity_geo`.

`query` uses it too, with the same switch, when the request asks for it: it finds the geo entities near the places of a question and gives them to the LLM that answers, within a radius you can set, and stores the places of the question it resolves (see [Nearby entities in `query`](#nearby-entities-in-query)).

## Turning it on

Off by default: it costs one more LLM call per text (an entity list is sent to the `detect_geo` stage) and it sends the names of
places to a geocoder.

* `geo = true` in `gligabao.toml`, or `serve --geo`: uses the public Nominatim geocoder of OpenStreetMap (cached; one request per
  second, as its [usage policy](https://operations.osmfoundation.org/policies/nominatim/) asks).
* A program that embeds gligabao sets `ServeOptions.GeoResolver` (any `gligabao.GeoResolver`); that turns geo on by itself.
  For Go code that builds the call itself: `memory_core.WithGeoResolver(r)`, and `api.WithGeoResolver(r)` for `api.Register`.

## What the stage does

`resolve_geo` runs after the text (every chunk, for a long text) is stored and before the web search. It never fails the call:
errors are logged, recorded in the trajectory and run log, and returned in `geo` of the result.

1. **Candidates**: the extracted entities that may be places (not a `person`, `concept`, `product` or `event`), at most 50,
   minus those that already have a row in `gb_entity_geo`: an entity is shared by every artifact that mentions it, so it is
   looked up once. A place the resolver did not know has no row, and is asked again the next time it is mentioned.
2. **Detect** (LLM, stage `detect_geo`): which candidates are places, their `kind` (`country`, `region`, `city` or `place`) and
   what to ask the resolver: the name plus what disambiguates it (`"Paris, France"`). It prefers precision: an entity it is
   not sure is a place is left out ("Bank of America" and "Paris Hilton" are not places).
3. **Resolve**: the geo resolver is asked for each place found (at most 25), one at a time. "Not found" is not a failure.
4. **Store**: one transaction writes the rows of `gb_entity_geo`.

`detect_geo` takes a model (`models`) and a reasoning level (`reasoning`) like the other LLM stages
([Remember](https://gligabao.mintlify.site/api-reference/remember.md#choosing-the-model-of-each-stage)). In the run log the stage is `resolve_geo` and holds the
`detect_geo` inference.

## In the trajectory

An [eval trajectory](https://gligabao.mintlify.site/evals/evals.md) of a call with a geo resolver has, after the steps of the text itself, a `select_geo_entities` step (the candidates), the `detect_geo` LLM call and a `detect_geo` step (the places it found, with `kind` and `query`), one `resolve_geo` step per place (found or not, the coordinates, `latency_ms`), and a `store_geo` step (how many places were stored). When there are no candidates, the stage stops after `select_geo_entities`.

## Nearby entities in `query`

With geo on, `query` makes one more LLM call, separate from the one that answers: the `detect_geo` stage reads the question (only the question, not the graph) and lists the places it mentions, each with the `text` as written, its `kind` and the `query` a geocoder should be asked ("Paris, France"). An LLM that names something that is not in the question is ignored, and at most 10 places are kept.

The search for nearby entities is **off by default**: nothing else happens unless the request turns it on, with `geo_nearby: true` or by setting `geo_radius_km` (setting a radius with `geo_nearby: false` is a `400`, and so is asking for it on a server without geo). You decide when nearby entities are involved. Then the answering LLM gets the geo entities near those places:

1. **Anchors** (at most 5), the places the search is centered on: first the entities that matched the question and have coordinates in `gb_entity_geo`; then the places found in the question that are not the name of one of those. For such a place, an entity of the dataset with that name that has coordinates is used as it is; otherwise the geo resolver is asked for coordinates, and a place it does not know is skipped.
2. **Places of the question are stored.** A place the resolver finds is stored: the entity of that name gets the coordinates if the dataset has one whose type can be a place (not a `person`, `concept`, `product` or `event`), otherwise a new entity of the type `place` is made. The next question with the same place finds it in the database and does not ask the resolver again, and it can be one of the nearby entities of later searches, not only places that `remember` stored. This is the one write of `query`, and happens only with the nearby search on; an entity made this way has no artifact and no description.
3. **Nearby**: for each anchor, the geo entities of the dataset within the radius (the 20 nearest per anchor, at most 30 in all; an entity near several anchors is listed once, with the nearest). The anchors that are entities are left out.
4. **Context**: the nearby entities and the edges around them (at most 50) are added to the graph the LLM reads, so it can cite them as evidence, and a `<nearby radius_km="50">` section says how far each is from which anchor (`Versailles is 17.9 km from Paris`).

The response of `query` has `nearby` (omitted when the search was off or there was no anchor):

```json theme={null}
{"radius_km": 50,
 "anchors": [{"name": "Lyon", "entity_id": 9, "stored": true, "latitude": 45.764, "longitude": 4.8357}],
 "nearby": [{"entity_id": 7, "name": "Villeurbanne", "type": "place", "latitude": 45.7665, "longitude": 4.8795, "distance_km": 3.4, "anchor": "Lyon"}]}
```

`stored` is only there for a place stored by this call.

**The radius** is 50 km by default. `geo_radius_km` in `gligabao.toml` sets the server's default (it does not turn the search on), and `geo_radius_km` in the [query request](https://gligabao.mintlify.site/api-reference/query.md) overrides it for one call. It must be greater than 0 and at most 20015 km (half the circumference of the Earth); `400` otherwise.

**How it is searched.** The geo index prefilters (SQLite: the R\*Tree; PostgreSQL: PostGIS `ST_DWithin` on the GiST index; without an index, the index on latitude and longitude), and the candidates are refined with the haversine distance on a sphere, so the result is the same either way. The antimeridian (a search near 180° also finds places at -179°) and the poles are handled. Distances are straight lines (great circle), not travel distances.

A failure of the step (the resolver, the database) is logged and recorded in the trajectory (a `nearby_geo` step), and never fails the query.

## The result

`geo` in the response of `remember` has one element per entity the LLM judged to be a place:

```json theme={null}
{"entity": "Paris", "entity_type": "place", "entity_id": 12, "kind": "city", "query": "Paris, France", "resolved": true,
 "latitude": 48.8588897, "longitude": 2.320041, "display_name": "Paris, Île-de-France, France", "country": "France",
 "country_code": "fr", "region": "Île-de-France", "provider": "nominatim"}
```

`resolved: false` with no `error` means the resolver does not know the place; with an `error` the resolver or the storage failed.

## The table

`gb_entity_geo` has one row per geo entity, keyed by `entity_id` (a foreign key to `gb_entities`; the row goes with the entity):

| Column | |
| - | - |
| `entity_id` | the entity (primary key) |
| `kind` | `country`, `region`, `city` or `place`, as the LLM judged it |
| `query` | what the resolver was asked for |
| `latitude`, `longitude` | degrees, WGS 84 (checked: -90..90, -180..180) |
| `display_name` | the full name the resolver gives |
| `country`, `country_code`, `region` | where the place lies (`country_code`: ISO 3166-1 alpha-2, lowercase); `''` if unknown |
| `place_type` | the resolver's own type (`city`, `administrative`, `peak`, ...) |
| `bbox_south`, `bbox_west`, `bbox_north`, `bbox_east` | bounding box; `NULL` if unknown |
| `importance` | how prominent the place is, 0..1; 0 if the resolver does not say |
| `provider`, `provider_ref` | the resolver and the place's ID there (`relation/7444`) |
| `created_at`, `updated_at` | |

Go: `graph.GetEntityGeo`, `graph.ListEntityGeo`, and `Tx.UpsertEntityGeo` / `Tx.GetEntityGeo` inside a transaction.

## Writing a resolver

```go theme={null}
type Resolver interface {
	Name() string
	Resolve(ctx context.Context, q geo.Query) (*geo.Place, error) // geo.ErrNotFound if it knows no such place
}
```

`geo.Cached(r)` adds a cache, `geo.NewNominatim(geo.NominatimOptions{BaseURL: ..., MinInterval: -1})` points at your own Nominatim
instance, and `geo.Func` adapts a function. A resolver must be safe for concurrent use and return coordinates in range
(`Place.Validate`), or the answer is treated as a failure.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.