Interactive job-listing visualiser backed by the Adzuna Jobs API, with geocoding via Nominatim (OpenStreetMap), persistence in SQLite (SQLAlchemy 2), and a live Leaflet map served by a pure-stdlib Python HTTP server.
jobmap/
βββ config/
β βββ settings.py # Typed env-var facade (single source of truth)
β βββ params.json # Live search parameters (R/W at runtime)
β
βββ src/
β βββ api/
β β βββ adzuna.py # Adzuna REST client (retry, pagination, typing)
β βββ geo/
β β βββ geocoder.py # Nominatim geocoder with SQLite cache
β βββ db/
β β βββ models.py # SQLAlchemy ORM: Job, GeoCache
β β βββ session.py # Engine, SessionFactory, init_db()
β βββ pipeline/
β β βββ ingest.py # End-to-end orchestrator (fetch β geocode β persist)
β βββ export/
β β βββ geojson.py # DB β RFC 7946 GeoJSON serialiser
β βββ server/
β βββ router.py # Decorator-based HTTP router
β βββ handler.py # BaseHTTPRequestHandler + route definitions
β
βββ templates/
β βββ map.html # Self-contained Leaflet SPA
β
βββ scripts/
βββ fetch_jobs.py # CLI: run the ingestion pipeline
βββ serve.py # CLI: start the development server
params.json
β
βΌ
AdzunaClient.search() βββββ Adzuna REST API
β
βΌ
CachingGeocoder.resolve_many() βββ Nominatim / GeoCache (SQLite)
β
βΌ
Session.merge(Job) ββββββββββββββββ SQLite (via SQLAlchemy)
β
βΌ
jobs_as_geojson() βββββββββββββββββ /api/jobs ββββ Leaflet map
| Requirement | Notes |
|---|---|
| Python β₯ 3.11 | Uses match, tomllib, PEP 695 generics |
| Adzuna API credentials | Free at developer.adzuna.com |
| Internet access | Adzuna API + Nominatim + OSM tile CDN |
# 1. Clone / unpack the project
cd jobmap
# 2. Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Configure credentials
cp .env.example .env
$EDITOR .env # Set ADZUNA_APP_ID and ADZUNA_APP_KEYStart the server, then open the browser:
python scripts/serve.py
# β http://127.0.0.1:8080/Use the sidebar widgets to set your search criteria and press Search & Fetch.
The application will:
- Write your parameters to
config/params.json. - Call the Adzuna API and geocode all results.
- Persist the enriched jobs to
data/jobmap.db. - Render the markers on the map.
Run the pipeline from the command line (useful for scheduled jobs):
# Edit params manually first
$EDITOR config/params.json
# Run the pipeline
python scripts/fetch_jobs.py --verbose
# Then start the server to view results
python scripts/serve.py| Variable | Default | Description |
|---|---|---|
ADZUNA_APP_ID |
required | Adzuna application ID |
ADZUNA_APP_KEY |
required | Adzuna application secret |
ADZUNA_COUNTRY |
gb |
ISO 3166-1 alpha-2 country code |
ADZUNA_RESULTS_PER_PAGE |
50 |
Results per API page (max 50) |
ADZUNA_MAX_PAGES |
5 |
Page fetch ceiling |
GEOCODER_USER_AGENT |
jobmap/1.0 |
Nominatim User-Agent |
GEOCODER_DELAY |
1.1 |
Inter-request delay (s) β Nominatim ToS |
DATABASE_URL |
sqlite:///data/jobmap.db |
SQLAlchemy connection URL |
SERVER_HOST |
127.0.0.1 |
Server bind address |
SERVER_PORT |
8080 |
Server bind port |
All fields mirror the Adzuna Search API parameters:
| Field | Type | Description |
|---|---|---|
what |
string | Keywords / job title |
where |
string | Location (city, postcode, β¦) |
distance |
integer | Search radius (km) |
salary_min |
integer | null | Minimum annual salary |
salary_max |
integer | null | Maximum annual salary |
contract_type |
string | null | permanent | contract | part_time | full_time |
category |
string | null | Adzuna category tag (e.g. it-jobs) |
sort_by |
string | relevance | date | salary |
max_pages |
integer | Pages to consume (overrides env var) |
| Path | Method | Body | Description |
|---|---|---|---|
/ |
GET | β | Serve map.html |
/api/params |
GET | β | Return current params.json |
/api/params |
POST | JSON | Overwrite params.json |
/api/jobs |
GET | β | GeoJSON FeatureCollection of geocoded jobs |
/api/fetch |
POST | β | Run ingestion pipeline; returns summary |
- Nominatim rate limiting β The geocoder enforces a β₯ 1.1 s inter-request delay and caches results in
data/jobmap.dbto comply with Nominatim's usage policy. Do not reduceGEOCODER_DELAYbelow 1.0. - Concurrent safety β The HTTP server uses
ThreadingHTTPServer; SQLAlchemy sessions are scoped per-call. Simultaneous fetch requests are serialised via a module-level lock. - Production use β This server is designed for local development. For production, place behind a reverse proxy (nginx / caddy) with authentication.