Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
84b18f8
Add pure fuzzy-search and by-ID geometry endpoints, decoupled from th…
yellowcap Jul 23, 2026
0c6586d
Include name and bbox in /search/fuzzy ids_only responses
yellowcap Jul 23, 2026
34b27af
Fix fuzzy-match ranking and broaden divisions_area coverage
yellowcap Jul 23, 2026
3faf809
Add subtype to /search/fuzzy ids_only responses
yellowcap Jul 23, 2026
6555d2a
Remove dead COUNTRIES and SUBTYPES literals from schemas.py
yellowcap Jul 24, 2026
692e6e8
Replace print() with logging in sql.py and search.py
yellowcap Jul 24, 2026
5504226
Unify geometry normalization and kill per-row DuckDB connections in e…
yellowcap Jul 24, 2026
ac7489a
Consolidate schema definition into config.SCHEMA_INFO
yellowcap Jul 24, 2026
42c4cad
Add GET /health endpoint
yellowcap Jul 24, 2026
925ffa4
Add request ID middleware and request logging
yellowcap Jul 24, 2026
87ca437
Add optional country and subtype to Place schema
yellowcap Jul 24, 2026
10597d3
Fix _per_source_limit to floor at 3 candidates for complex queries
yellowcap Jul 24, 2026
0e8628d
Unify /search/fuzzy response shape for ids_only mode
yellowcap Jul 24, 2026
5ff62f8
Optimize to_feature_collection to use to_dict(orient=records)
yellowcap Jul 24, 2026
df4a3f3
Add GET /sources endpoint
yellowcap Jul 24, 2026
11e0d81
Fix lint: remove unused math import, move imports before constants in…
yellowcap Jul 24, 2026
8429e5e
add unit tests and CI workflow
yellowcap Jul 24, 2026
3245c3b
Add mypy type checking to CI and fix type errors
yellowcap Jul 24, 2026
d4fae95
Drop 3.14 from CI test matrix
yellowcap Jul 24, 2026
61f1033
Fix ruff formatting across all source and test files
yellowcap Jul 24, 2026
ad8201c
Add ruff.toml and fix lint errors (imports, type hints, formatting)
yellowcap Jul 24, 2026
89d539f
Add minimal parquet fixture data for CI tests
yellowcap Jul 24, 2026
943ec7e
fix: use module-scoped con fixture to avoid repeated LOAD spatial ove…
yellowcap Jul 24, 2026
059dfce
fix: point NATURAL_EARTH_PATH to test_ne_geography.parquet tracked in…
yellowcap Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install ruff
run: pip install ruff
- name: Lint
run: ruff check src/ tests/
- name: Format check
run: ruff format --check src/ tests/
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Sync dependencies
run: uv sync --all-extras
- name: Type check
run: uv run mypy --explicit-package-bases src/gazet tests/

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Sync dependencies
run: uv sync --all-extras
- name: Run tests
run: uv run pytest tests/ -v --tb=short
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ For now, gazet relies on [ollama](https://ollama.com/). For remote (cloud) model
```bash
python -m gazet
# then GET http://localhost:8000/search?q=Border%20between%20Loja%20and%20Piura
# pure fuzzy match, no LLM, raw geometry only:
# GET http://localhost:8000/search/fuzzy?q=Loja&limit=5
# cheap ID-only search, then fetch geometry for just the one you want:
# GET http://localhost:8000/search/fuzzy?q=Loja&limit=5&ids_only=true
# GET http://localhost:8000/geometry/25f85439-e054-44e3-9ad4-9fbf755e3183
```

### API + Streamlit demo
Expand All @@ -77,13 +82,18 @@ uv run streamlit run gazet_demo.py # demo UI
| `schemas.py` | `SUBTYPES`, `COUNTRIES`, `Place`, `PlacesResult` |
| `lm.py` | DSPy signatures + LM init (`extract`, `write_sql`) |
| `search.py` | fuzzy search against `divisions_area` / `natural_earth` |
| `geometry.py` | geometry simplification / GeoJSON normalization (no LLM dependency) |
| `sql.py` | code-act SQL generation loop |
| `export.py` | GeoJSON FeatureCollection writer |
| `api.py` | FastAPI app with `/search?q=...` returning GeoJSON FeatureCollection |
| `api.py` | FastAPI app: `/search` (LLM), `/search/fuzzy` (fuzzy-only), `/geometry/{id}` (direct ID lookup) |

## Design notes

- `api.py` exposes GET `/search?q=<query>`; returns GeoJSON FeatureCollection and logs intermediate output.
- `api.py` exposes GET `/search?q=<query>`; returns GeoJSON FeatureCollection and logs intermediate output. This path runs two LLM calls (place extraction, then SQL generation for candidate selection/disambiguation and GIS ops).
- `api.py` also exposes GET `/search/fuzzy?q=<place name>&limit=5&simplify=true&sources=divisions_area,natural_earth` — pure Jaro-Winkler fuzzy match with no LLM involved. `q` is a place-name string, not a natural-language query (there's no place-extraction step). Returns the combined top-`limit` matches across sources as a GeoJSON FeatureCollection with raw (or simplified, if `simplify=true`) geometry attached. Pass `ids_only=true` to skip full geometry and get back `{"ids": [{"source", "id", "name", "country", "subtype", "admin_level", "bbox"}, ...]}` — `country`/`subtype`/`admin_level` disambiguate same-named places (e.g. multiple real "Loja"s across Ecuador and Spain, or Ecuador's "Loja" region vs. its nested "Loja" county), and `bbox` is `[minx, miny, maxx, maxy]`, a much smaller payload than full geometry, for picking a candidate before fetching its full geometry.
- Fuzzy matching (`search.py`) ranks exact-substring matches (query literally contained in the name, e.g. "Loja" in "Loja Province") ahead of pure edit-distance near-misses (e.g. "Lodja"), even when the near-miss scores a slightly higher raw Jaro-Winkler similarity. `divisions_area` matches against `names.primary` (not `names.common.en`, which is null for 77% of rows and would otherwise exclude most of the dataset from every fuzzy search).
- `api.py` also exposes GET `/geometry/{id}?source=divisions_area&simplify=true` — fetch a single feature's geometry directly by ID, no fuzzy matching or LLM involved. `source` is inferred from the ID if omitted (Natural Earth IDs are prefixed `ne_`). Returns a single GeoJSON Feature, 404 if not found.
- The spatial extension is loaded once at API startup (FastAPI `lifespan`) onto a shared DuckDB connection stored on `app.state`; each request gets a cheap `cursor()` off it instead of paying DuckDB's ~90ms `LOAD spatial` cost per call.
- LM is initialised at import time in `lm.py`, suitable for a long-lived server process.
- Data lives in `data/overture/` and `data/natural_earth_geoparquet/` (not tracked in git).

Expand Down
Binary file not shown.
Binary file added data/overture/divisions_area/test.parquet
Binary file not shown.
2 changes: 1 addition & 1 deletion gazet_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def _track(event_name: str, **props):
st.caption("Built by")
st.image(DEVSEED_LOGO_PATH, width=180)

backend = "gguf"
backend = os.environ.get("GAZET_BACKEND", "gguf")

if "run_q" not in st.session_state:
st.session_state.run_q = None
Expand Down
35 changes: 34 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,47 @@ dependencies = [
"httpx>=0.28.1",
"sqlparse>=0.5.5",
]
optional-dependencies = { demo = ["streamlit", "requests", "pydeck"], dev = ["ruff"] }
optional-dependencies = { demo = ["streamlit", "requests", "pydeck"], dev = ["ruff", "mypy>=1.15", "pandas-stubs>=2.0", "pytest>=8.0", "pytest-asyncio>=0.24"] }

[project.scripts]
gazet-dataset = "dataset.scripts.cli:main"

[tool.hatch.build.targets.wheel]
packages = ["src/gazet", "dataset"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = ["-ra", "--strict-markers"]

[tool.mypy]
python_version = "3.13"
mypy_path = "src"
strict = true
disallow_any_generics = false # pandas-stubs use Any heavily
warn_return_any = false # dspy returns Any; noisy without value
disallow_untyped_defs = false # too noisy on test files

[[tool.mypy.overrides]]
module = "dspy.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "duckdb.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "tests.*"
check_untyped_defs = false
disallow_untyped_defs = false
warn_return_any = false

[[tool.mypy.overrides]]
module = "gazet.lm"
# dspy has no stubs — its classes (Signature, Module, Predict, LM) are all typed as Any.
# Suppressing the resulting noise while keeping the rest of the codebase strict.
disable_error_code = ["misc", "no-untyped-def", "no-untyped-call"]

[dependency-groups]
dataset = [
"modal>=1.4.0",
Expand Down
7 changes: 7 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
target-version = "py313"

[lint]
# BLE001 – catching Exception is intentional in health checks and error-handling paths
# S110 – try/except/pass is intentional for health probes that must not crash
# B017 – pytest.raises(Exception) is acceptable for pydantic validation error checks
ignore = ["BLE001", "S110", "B017"]
Loading
Loading