Skip to main content

Natural language geographic query parsing using LLMs

Project description

etter logo

etter /ˈɛtɐ/ n. (Swiss German) — the boundary or enclosure marking the edge of a village or commune; a natural demarcation between settled and unsettled land.

Natural language geographic query parsing using LLMs.

Overview

etter transforms natural language location queries into structured geographic filters that can be used by search engines and spatial databases. It uses Large Language Models (LLMs) to understand multilingual queries and extract spatial relationships.

Key Principle: etter's sole purpose is to extract the geographic filter from user queries. It does NOT handle feature/activity identification or search execution.

[!TIP] Documentation available at https://geoblocks.github.io/etter/

Sponsorship

camptocamp logo

The development of this library is sponsored by Camptocamp.

Features

  • Geographic Filters Only: Extracts spatial relationships from queries, ignoring non-geographic content
  • Multilingual Support: Parse queries in English, German, French, Italian, and more
  • Rich Spatial Relations: Support for containment, buffer, and directional queries
  • Structured Output: Pydantic models with full type safety
  • Streaming Support: Real-time feedback with reasoning transparency for responsive UIs
  • Flexible Configuration: Customizable spatial relations and confidence thresholds
  • LLM Provider Agnostic: Works with OpenAI, Anthropic, or local models

What etter Does (and Doesn't Do)

✅ etter extracts:

  • Spatial relations: "north of", "in", "near", etc.
  • Reference locations: "Lausanne", "Lake Geneva", etc.
  • Distance parameters: "within 5km", "around 2 miles", etc.

❌ etter does NOT handle:

  • Feature/activity identification: "hiking", "restaurants", "hotels"
  • Attribute filtering: "with children", "vegetarian", "4-star"
  • Search execution or database queries

Integration Pattern: Parent application handles feature/activity filtering and combines it with etter's geographic filter for complete search functionality.

Installation

pip install etter

With PostGIS datasource support:

pip install etter[postgis]

Development setup

This project uses uv for dependency management.

git clone https://github.com/geoblocks/etter.git
cd etter
uv sync --extra dev

Demos

For all the demos, set a valid model name and API key in .env file:

cat <<EOF > .env
LLM_API_KEY="sk-..."
LLM_MODEL="gpt-4o"
EOF

REPL

An interactive REPL is available for testing queries interactively:

uv run python repl.py

Demo API Server

A FastAPI demo server is available that combines query parsing with geographic resolution using SwissNames3D data.

uv run uvicorn demo.main:app --port 8000 --reload

The API will be available at http://localhost:8000.

Quick Start

from langchain.chat_models import init_chat_model
from etter import GeoFilterParser
import os

# Initialize LLM
llm = init_chat_model(
    model=os.getenv("LLM_MODEL", "gpt-4o"),
    temperature=0,
    api_key=os.getenv("LLM_API_KEY")
)

# Initialize parser
parser = GeoFilterParser(
    llm=llm,
    confidence_threshold=0.6,
    strict_mode=False
)

# Strict mode - raises error on low confidence
parser = GeoFilterParser(
    llm=llm,
    confidence_threshold=0.8,
    strict_mode=True
)

Custom Spatial Relations

from etter import SpatialRelationConfig, RelationConfig

config = SpatialRelationConfig()
config.register_relation(RelationConfig(
    name="close_to",
    category="buffer",
    description="Very close proximity",
    default_distance_m=1000,
    buffer_from="center"
))

parser = GeoFilterParser(spatial_config=config)

API Reference

GeoFilterParser

Main class for parsing queries.

Methods:

  • parse(query: str) -> GeoQuery: Parse a single query
  • aparse(query: str) -> GeoQuery: Async version of parse (awaits ainvoke on the LLM)
  • parse_stream(query: str) -> AsyncGenerator[dict]: Parse with streaming events
  • parse_batch(queries: List[str]) -> List[GeoQuery]: Parse multiple queries
  • get_available_relations(category: Optional[str]) -> List[str]: List available relations
  • describe_relation(name: str) -> str: Get relation description

Constructor options (selected):

  • confidence_threshold: Minimum confidence to accept (0–1, default 0.6)
  • strict_mode: Raise LowConfidenceError instead of warning (default False)
  • include_examples: Include few-shot examples in the prompt (default True)
  • datasource: GeoDataSource instance — informs the LLM of available concrete types
  • additional_instructions: Free-form text injected as a system message after the main prompt and before few-shot examples. Use for region-specific endonyms, domain aliases, or organization-specific place names.

GeoQuery

Structured output model representing the parsed geographic filter.

Attributes:

  • query_type: Type of query (simple, compound, split, boolean)
  • spatial_relation: Spatial relationship (e.g., "north_of", "in", "near")
  • reference_location: Reference location (e.g., "Lausanne"), or None for attribute-only queries
  • buffer_config: Buffer parameters (optional)
  • confidence_breakdown: Confidence scores
  • original_query: Original input text

Note: etter is fully implemented with three integrated layers: parsing, geographic resolution via datasources, and spatial operations. The demo API shows a complete end-to-end workflow that resolves locations and computes search areas.

Available Spatial Relations

Containment

  • in: Exact boundary matching

Buffer/Proximity

  • near: Proximity with context-aware distance (default 5km, LLM infers based on activity, feature scale, and intent)
  • on_shores_of: 1km ring buffer (excludes water body)
  • along: 500m buffer for linear features
  • left_bank, right_bank: Buffer on one side of a linear feature (river, road) relative to its flow direction
  • in_the_heart_of: Erosion for central areas (default -500m, LLM infers based on area size)

Directional

  • Cardinal: north_of, south_of, east_of, west_of: 10km sector (90° each)
  • Diagonal: northeast_of, southeast_of, southwest_of, northwest_of: 10km sector (90° each)

Error Handling

from etter import ParsingError, UnknownRelationError, LowConfidenceError

try:
    result = parser.parse("some query")
except ParsingError as e:
    print(f"Failed to parse: {e}")
    print(f"Raw LLM response: {e.raw_response}")
except UnknownRelationError as e:
    print(f"Unknown relation: {e.relation_name}")
except LowConfidenceError as e:
    print(f"Low confidence: {e.confidence}")
    print(f"Reasoning: {e.reasoning}")

Demo Examples

Here are some good example queries to try with the demo application:

  • walk in the Gros-de-Vaud
  • on the shores of the lac Morat
  • near Lausanne
  • south west of Lausanne
  • 5km north of Lausanne
  • walking distance from Zurich main railway station
  • 15 min biking from Zurich main railway station
  • along l'Orbe
  • 2km right bank of the Rhône

Architecture

See ARCHITECTURE.md for detailed system design.

Development

# Install dev dependencies
uv sync --extra dev

# Run tests
uv run pytest

# Format code
uv run ruff format

# Linting
uv run ruff check

# Type checking
uv run ty check

Releases

Releases are automated via Release Please. The version and changelog are determined by PR titles using the Conventional Commits format:

PR title prefix Version bump
feat: ... minor (0.x.0)
fix: ..., perf: ... patch (0.0.x)
feat!: ... or BREAKING CHANGE major (x.0.0)

All other prefixes (chore:, docs:, refactor:, test:, ci:) do not trigger a release.

When a releasable commit lands on main, Release Please opens a release PR. Merging it tags the commit and triggers the PyPI publish workflow.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

etter-0.4.1.tar.gz (68.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

etter-0.4.1-py3-none-any.whl (55.1 kB view details)

Uploaded Python 3

File details

Details for the file etter-0.4.1.tar.gz.

File metadata

  • Download URL: etter-0.4.1.tar.gz
  • Upload date:
  • Size: 68.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for etter-0.4.1.tar.gz
Algorithm Hash digest
SHA256 8058732f176ec708ff9cb33b3934b1ad87c28043b2d0de9e743585ebd2cd107b
MD5 91d272c4374134b98e8aac5ea23be01a
BLAKE2b-256 c73795151bcc4e023c58043af373477a76a00cf36f2bd8f76eef1bee1c04bad0

See more details on using hashes here.

File details

Details for the file etter-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: etter-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 55.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for etter-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 695c2e7a36c6b23011aa32111459b2aa81a5d9d97e84cb1a53053c3904acfdfd
MD5 5999a0ac98f6e9944ba6faf75b08baa4
BLAKE2b-256 a15c3bab4639c48bf5d8ad9226a6f4e389f59991f355b9e954a22046c5162a46

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page