Fine-tune a small language model to do natural-language geocoding, in two Colab notebooks and about two hours. This is a teaching-sized reproduction of gazet: one country (Ecuador), one table (Overture administrative divisions), eight query templates, a 0.8B model with a LoRA adapter trained on a free T4.
"which cantons border Cuenca?"
-> model extracts ["Cuenca"]
-> fuzzy search returns 10 candidate rows (Cuenca, Cuenca?, Cumandá, ...)
-> model writes DuckDB spatial SQL using the right id
-> SQL runs, polygons come back as GeoJSON
No accounts needed. No Hugging Face token, no Drive mount. The dataset produced by
notebook 1 is bundled with the package, so notebook 2 runs on its own; a trained
adapter can be downloaded (SKIP_TRAINING = True) if you only want the evaluation and demo.
Training data is entirely synthetic and never touches an LLM. Each SQL template comes with a list of question phrasings; a sampled place fills both, the SQL is executed against the real table, and only queries that return rows are kept.
| family | template | example question |
|---|---|---|
| direct_lookup | lookup_01 |
where is Latacunga? |
| disambiguation | disambiguate_01 |
Bolívar, Carchi (there are three Bolívars) |
| adjacency | adj_01, adj_02 |
which provinces border Pichincha? |
| containment | contain_01, contain_02 |
cantons of Azuay / which province is Baños in? |
| buffer | buffer_01 |
cantons within 25 km of Guayaquil |
| aggregation | agg_01 |
3 largest cantons in Loja |
One model is trained on two tasks at once, distinguished by the system prompt: extract place names as JSON, and write SQL given a candidate table plus the question. The evaluation metric is execution accuracy: predicted and reference SQL are both run, and the sets of returned feature ids must match.
| time | block |
|---|---|
| 0:00–0:10 | What gazet does; why synthetic data; what we simplify |
| 0:10–0:45 | Notebook 1 live: data, relations, templates, generation, export |
| 0:45–1:15 | Notebook 2: install, LoRA, masking check, start training; discuss the eval while it runs |
| 1:15–1:40 | Results: base vs fine-tuned, error gallery |
| 1:40–1:55 | Live geocoding on maps; how it ships (GGUF, llama-server) |
| 1:55–2:00 | Homework: 2 epochs, unseen-canton holdout, add a template, change country |
notebooks/ the two .ipynb files Colab opens (generated from notebooks/src/*.py)
notebooks/src/ percent-format sources; edit these, then scripts/build_notebooks.sh
src/gazet_tutorial/ helper package: DuckDB/Overture access, fuzzy search, SQL utils, eval, maps
artifacts/ bundled output of notebook 1 (dataset, prompts, relation tables)
tests/ pytest suite (synthetic square polygons, no downloads)
scripts/ build_notebooks.sh, run_nb1.sh (regenerates artifacts/ via papermill)
The learner-facing logic (templates, generation loop, prompts, training config,
metrics, the geocode() function) lives in notebook cells. The package holds only
boilerplate so that pip install is the entire setup.
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e ".[dev]"
.venv/bin/python -m pytest
scripts/run_nb1.sh # executes notebook 1 (downloads ~10 MB from Overture S3)
scripts/run_nb1.sh "/path/to/overture/divisions_area/*.parquet" # or from a local copyNotebook 2 needs a CUDA GPU (Unsloth does not run on Apple Silicon); run it in Colab.
artifacts/dataset/is produced byscripts/run_nb1.shwith fixed seeds; regenerate it whenever templates or prompts change.- The trained adapter for
SKIP_TRAININGis attached to the GitHub releasev1asadapter.zip(produced by notebook 2 with default settings).
Code is MIT licensed. Boundary data comes from the Overture Maps Foundation divisions theme, which is derived from OpenStreetMap and distributed under the ODbL; the bundled dataset contains feature ids and names from that data and inherits its terms. Model weights: Qwen3.5 (Apache 2.0), via Unsloth.