Skip to content

Repository files navigation

GREG — Geo Resolution Engine Gazetteer

A RAG-based system for resolving ambiguous, partial, or misspelled location names to structured geographic data. Country resolution is fully implemented today; city/state resolution is a backlog item — see ROADMAP.md for what's built vs planned, and CONVENTIONS.md for the tradeoffs behind how it's built.

Why "GREG"? A gazetteer is the actual term for a geographic name index/reference — which is exactly what this project builds and resolves against. "Geo Resolution Engine Gazetteer" backronyms cleanly to GREG: technically accurate, but easier to say out loud than "geo-resolution-rag."

Features

  • Fuzzy matching: Handles typos and spelling variations ("Jermany" → Germany)
  • Multilingual: Recognizes translations and aliases ("alemania" → Germany, "japon" → Japan)
  • Disambiguation: An LLM picks the best match from pgvector-retrieved candidates, not a naive nearest-neighbor lookup
  • Resilient: Falls back to Nominatim (OpenStreetMap) geocoding when the RAG pipeline has no confident match, instead of failing outright
  • Structured output: Returns name, ISO codes, capital, region/subregion, confidence, and the reasoning behind the match

Prerequisites

The CLI (src/main.py) always runs locally via uv run; it needs Ollama and PostgreSQL/pgvector reachable. The HTTP API (src/api.py) can either run locally the same way, or you can let Docker serve it too. Pick one:

Option A: Docker (recommended)

docker compose up -d

This starts postgres (pgvector, with migrations/*.sql auto-applied on first init — see the Database Setup note below), ollama, a one-shot ollama-init job that pulls mistral:latest and nomic-embed-text into a persisted volume, and api — the HTTP API from a Dockerfile build, which waits for Postgres to be healthy and ollama-init to finish before it starts, so it's ready to actually resolve queries as soon as it comes up rather than 500ing on a missing model. First run takes a few minutes for the model pulls; docker compose ps shows when ollama-init has exited (status 0) and when api is healthy. Re-running docker compose up later is cheap — ollama pull no-ops once a model is cached, and --build only rebuilds api if src/, pyproject.toml, or uv.lock changed.

Uses the public ports 11434 (Ollama), 5432 (Postgres, override with POSTGRES_PORT), and 8000 (API), so the defaults in .env.example work unchanged. The CLI still needs uv sync run locally (see Quick Start) even if you only use Docker for its dependencies — it isn't containerized.

Option B: Native install

Install Ollama, pull models, and set up PostgreSQL/pgvector by hand

Install Ollama from ollama.com or via command line:

# Linux
curl -fsSL https://ollama.com/install.sh | sh

# macOS (via Homebrew)
brew install ollama

Pull the required models:

ollama pull mistral:latest
ollama pull nomic-embed-text

Start the Ollama server:

ollama serve

Install PostgreSQL with pgvector:

# Ubuntu/Debian
sudo apt install postgresql-16-pgvector

# Or run just the Postgres container from docker-compose.yml:
docker compose up -d postgres

Quick Start

# Install dependencies (requires uv: https://docs.astral.sh/uv/)
uv sync

# Set up environment (optional - defaults work for local setup)
cp .env.example .env

Database Setup

If you used docker compose up -d, the tables already exist — Postgres auto-runs everything in migrations/ on first init of an empty data volume. Otherwise, apply them by hand:

psql -h localhost -U postgres -d geo_resolution -f migrations/001_create_cities_table.sql
psql -h localhost -U postgres -d geo_resolution -f migrations/002_create_countries_table.sql
psql -h localhost -U postgres -d geo_resolution -f migrations/003_create_states_table.sql
psql -h localhost -U postgres -d geo_resolution -f migrations/004_create_resolution_feedback_table.sql

(This also applies if you're reusing an existing postgres_data volume from before this table was added — auto-init only runs against an empty volume, so run the migration manually in that case.)

Data Indexing

Before using the application, you must index the geographic data. This step generates vector embeddings for all locations using the nomic-embed-text model and stores them in PostgreSQL:

# Index countries (~250 records)
uv run python -m src.loaders.countries

# Index states/provinces (~5K records)
uv run python -m src.loaders.states

# Index cities (~150K records - this may take a while)
uv run python -m src.loaders.cities

Note: The indexing process embeds all geographic data using the Ollama embedding model. The cities indexing may take significant time depending on your hardware.

Run the Application

# CLI (single-shot, see Usage below)
uv run python -m src.main "<country query>"

# HTTP API, at http://127.0.0.1:8000 (interactive docs at /docs)
# Already running if you used `docker compose up -d` (Option A above) — this
# is only needed for local development or if you used Option B.
uv run uvicorn src.api:app --reload

Environment Variables

Create a .env file with the following (all have sensible defaults):

# Ollama (defaults to localhost)
OLLAMA_BASE_URL=http://localhost:11434

# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=geo_resolution
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

# Nominatim geocoding fallback (used when the RAG pipeline has no confident match)
NOMINATIM_BASE_URL=https://nominatim.openstreetmap.org
NOMINATIM_USER_AGENT=geo-resolution-rag/0.1

Usage

src/main.py is a single-shot CLI: pass a country query as an argument, get one resolution back.

uv run python -m src.main "alemania"
Resolving: 'alemania'
--------------------------------------------------
Time: 1.34s
--------------------------------------------------
Match: Germany (DE)
Official: Federal Republic of Germany
Capital: Berlin
Region: Europe, Western Europe
Confidence: 95%
Reason: Alemania is the Spanish translation for Germany

HTTP API

src/api.py (FastAPI) exposes the same resolution over HTTP, for anything that isn't a shell — scripts, other services, etc. Start it with uv run uvicorn src.api:app --reload, then:

curl "http://127.0.0.1:8000/v1/countries/resolve?q=alemania"
{
  "matched": true,
  "name": "Germany",
  "official_name": "Federal Republic of Germany",
  "iso2": "DE",
  "iso3": "DEU",
  "capital": "Berlin",
  "region": "Europe",
  "subregion": "Western Europe",
  "confidence": 0.95,
  "reason": "Alemania is the Spanish translation for Germany"
}

q is required; k (default 5, range 1-20) optionally overrides how many RAG candidates are retrieved: ?q=alemania&k=10. GET /health is a liveness check that doesn't touch Ollama/Postgres. Interactive OpenAPI docs are served at /docs while the API is running.

Example Resolutions

Input Output Reasoning
alemania Germany Spanish translation, resolved via RAG
japon Japan French translation, resolved via RAG
Jermany Germany Typo correction, resolved via RAG
brasil Brazil Portuguese spelling, resolved via RAG
a query the RAG pipeline can't confidently place best-effort match Nominatim fallback

Architecture

User Input (CLI: src/main.py, or HTTP: src/api.py)
    ↓
┌─────────────────┐
│  Ollama         │  Convert query to vector
│  nomic-embed    │  (nomic-embed-text model)
└────────┬────────┘
         ↓
┌─────────────────┐
│  PostgreSQL     │  pgvector similarity search
│  pgvector       │  (250 countries indexed)
└────────┬────────┘
         ↓ top-k candidates
┌─────────────────┐
│  Ollama         │  Disambiguate and select best match
│  Mistral LLM    │  (mistral:latest model)
└────────┬────────┘
         ↓
   matched=False or low confidence?
         ↓ yes                    ↓ no
┌─────────────────┐               │
│  Nominatim      │               │
│  fallback       │               │
└────────┬────────┘               │
         ↓                        ↓
┌───────────────────────────────────────┐
│  CountryResult                        │  {name, iso2/iso3, capital,
│  (also logged to resolution_feedback) │   region, confidence, reason}
└───────────────────────────────────────┘

See CLAUDE.md for the module-by-module breakdown (src/resolver.py, src/fallback.py, src/feedback.py, etc.).

Data Sources

  • countries.csv: 250 countries with multilingual names — the only one actively resolved today.
  • cities.csv: 150,000+ cities with coordinates — loaded and embedded, but not yet resolvable end-to-end (see ROADMAP.md).
  • states.csv: 5,000+ states/provinces — same status as cities.

Requirements

  • Python 3.10+
  • Ollama with mistral:latest and nomic-embed-text models
  • PostgreSQL 14+ with pgvector extension

License

MIT

About

Geo-resolution engine that maps ambiguous, misspelled, or multilingual place names to structured geographic data. Python RAG over pgvector, LLM disambiguation, Nominatim fallback. Country resolution shipped; cities on the roadmap.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages