- Python 81.7%
- Vue 7.8%
- Jinja 4%
- TypeScript 2.2%
- Lua 1.4%
- Other 2.9%
POI Complete
POI Complete is a Python project whose main goal is to help improving the data coverage and quality of OpenStreetMap (OSM) point of interests (POIs). From OSM POIs, it crawls associated websites, uses ML/LLM to extract opening hours information, and provides a REST API for reviewing predictions.
Features
- Extract POI data from OSM PBF files (imported via osm2pgsql)
- Crawl associated websites to extract content (save as HTML and markdown)
- Use ML/LLM (pydantic-ai with OpenRouter) to extract opening hours
- Store results in PostgreSQL
- REST API for reviewing predictions
- Vue.js frontend for annotation workflow
Tech Stack
- Python: 3.12+
- Database: PostgreSQL with Peewee ORM
- Web Framework: FastAPI
- Crawler: Crawlee
- ML/LLM: pydantic-ai with OpenRouter
- CLI: Typer
- Frontend: Vue.js 3 with TypeScript, Pinia, Vite, vue-i18n
Project Structure
src/poi_complete/
├── __main__.py # CLI entry point with all commands
├── api.py # FastAPI REST endpoints
├── config.py # Settings via pydantic-settings
├── models.py # Peewee database models
├── types.py # Enums and dataclasses
├── utils.py # Utility functions
├── parse.py # HTML parsing utilities
├── crawl.py # Web crawler implementation
└── predictions/
├── importer.py # Prediction/insight import logic
└── opening_hours/ # Opening hours extraction
├── tasks.py
├── schemas.py
└── utils.py
Database Tables
| Table | Description |
|---|---|
pois |
POI data from OSM |
website_crawls |
Crawl job tracking |
crawl_results |
Crawled content (URL, markdown, HTML path) |
predictions |
ML predictions from content |
insights |
Curated insights for user review (accept/reject) |
Installation
Prerequisites
- Python 3.12+
- PostgreSQL
- Node.js (for frontend)
Backend Setup
uv is recommended for managing the Python environment.
-
Create the virtualenv and install dependencies:
uv sync -
Configure environment variables
This is optional, as we provide sensible defaults for a local setup.
- Create database tables:
poi-complete create-tables
This create all tables except pois, which is created by osm2pgsql when importing OSM data (see below).
OSM Data import
POI data is imported from OSM PBF files using osm2pgsql with a custom Lua configuration.
The import process uses the Lua configuration at config/pois.lua, which:
- Imports POIs from OSM data with keys:
amenity,shop,tourism,leisure,office,craft,historic,man_made,aeroway,healthcare - Extracts common attributes: name, opening_hours, website, phone, operator, addr_city
- Stores all tags in a JSONB column for flexibility
Prerequisites:
- An OSM PBF file (e.g., from Geofabrik)
Import command:
docker compose run --rm osm2pgsql \
--host postgis \
--database postgres \
--user postgres \
--style /config/pois.lua \
-O flex \
--slim \
/pbf/your-region.osm.pbf
The import creates the pois table with the following schema:
| Column | Type | Description |
|---|---|---|
id |
bigint | OSM ID (negative for ways/relations) |
category |
text | OSM key (e.g., amenity, shop) |
type |
text | OSM value (e.g., restaurant, cafe) |
name |
text | POI name |
opening_hours |
text | OSM opening_hours tag |
website |
text | Website URL |
phone |
text | Phone number |
operator |
text | Operator |
addr_city |
text | City from address |
tags |
jsonb | All OSM tags |
geom |
geometry(Point,4326) | Centroid geometry |
Note: The pois table is created automatically by osm2pgsql during import.
Partial update
To update our local database to be synchronized with OSM, we use the osm2pgsql-replication CLI.
We first need to run the initialization:
docker compose run --rm osm2pgsql osm2pgsql-replication \
init \
--host postgis \
--database postgres \
--user postgres
Then, on a regular basis (e.g., daily), we can run the update command:
docker compose run --rm osm2pgsql osm2pgsql-replication \
update \
--host postgis \
--database postgres \
--user postgres \
--verbose -- \
--style /config/pois.lua \
-O flex
Frontend Setup
cd front
npm install
Configuration
Environment variables:
| Variable | Description | Default |
|---|---|---|
DB_PORT |
PostgreSQL port | 5432 |
DB_NAME |
Database name | postgres |
DB_USER |
Database user | postgres |
DB_PASSWORD |
Database password | postgres |
DB_HOST |
Database host | localhost |
LLM_API_KEY |
OpenRouter API key | - |
LLM_MODEL |
LLM model for extraction | minimax/minimax-m2.5 |
STORAGE_PATH |
Path for HTML/screenshot storage | ./data |
Usage
CLI Commands
# Create database tables
poi-complete create-tables
# Run crawler on all POIs
poi-complete crawl --batch-size 100 --max-concurrency 8
# Run crawler with filters and advanced options
poi-complete crawl \
--filter-category amenity \
--filter-type restaurant \
--max-retries 3 \
--max-crawl-depth 1 \
--enable-poi-rate-limit
# Crawl specific POI
poi-complete crawl-poi n123456789
# Extract opening hours from crawl result
poi-complete extract-opening-hour <crawl_result_id>
# Generate insights from all crawl results
poi-complete generate-opening-hours-insights --limit 1000
# Generate insights for POIs missing OSM data only
poi-complete generate-opening-hours-insights --missing-only
# Refresh insights for all POIs
poi-complete refresh-insights
# Drop a table
poi-complete drop-table <table_name>
Crawl Command Options
| Option | Description |
|---|---|
--filter-category |
Filter POIs by OSM category (e.g., amenity, shop, tourism) |
--filter-type |
Filter POIs by OSM type (e.g., restaurant, cafe, museum) |
--batch-size |
Number of POIs per batch (default: 100) |
--max-concurrency |
Maximum concurrent crawling tasks (default: 50) |
--desired-concurrency |
Target concurrency (defaults to max-concurrency if not set) |
--max-retries |
Maximum retries for failed requests (default: 0) |
--max-crawl-depth |
Maximum link hops from main page (default: 0) |
--enable-poi-rate-limit |
Enable rate limiting per POI |
--limit |
Maximum total POIs to crawl |
API Endpoints
# Get insights (with optional filters)
GET /api/v1/insights?annotated=false&types=opening_hours&limit=100&missing=true
# Parameters:
# - annotated: Filter by annotation status (true/false/null)
# - types: Filter by insight types (comma-separated)
# - limit: Number of results (1-250, default: 100)
# - missing: If true, only insights adding new OSM data; if false, only updates
# Annotate an insight (accept=1, reject=0)
POST /api/v1/insights/{insight_id}/annotate?annotation=1
Running the Backend API
uvicorn poi_complete.api:app --reload
A make command is also available for convenience:
make up-server
The API runs on http://localhost:8000.
Running the Frontend
cd front
npm run dev
A make command is also available for convenience:
make up-front
The frontend runs on http://localhost:5173 and proxies API requests to the backend.
Frontend Features
- Displays one insight at a time for review
- Shows POI name, website URL, current vs predicted opening hours
- Shows raw text used for prediction and LLM comments
- Indicates whether insight adds new data or updates existing OSM data
- Accept/Reject buttons to annotate insights
- Progress bar showing position in the batch
- Auto-loads more insights when running out
- Filter insights by missing OSM data status
- Open in JOSM button to directly edit in OSM editor
Code Quality
Linting/Formatting
ruff check .
ruff format .
isort src/ --profile black
Type Checking
pyright src/
Adding a New Prediction Type
- Add type to
types.py(PredictionType enum) - Create prediction extraction logic in
predictions/ - Create importer class in
predictions/importer.py(subclassInsightImporter) - Add importer to
IMPORTERSlist - Add CLI command if needed in
__main__.py
Adding Translations
- Add translation keys to
front/src/i18n/en.json - Add corresponding translations to
front/src/i18n/fr.json - Use in components with
t('key.path')