yoro-maps 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build distribution
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+
18
+ - name: Install build tools
19
+ run: pip install build
20
+
21
+ - name: Build wheel and sdist
22
+ run: python -m build
23
+
24
+ - name: Upload artifacts
25
+ uses: actions/upload-artifact@v4
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+
30
+ publish:
31
+ name: Publish to PyPI
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment: pypi
35
+ permissions:
36
+ id-token: write
37
+ steps:
38
+ - name: Download artifacts
39
+ uses: actions/download-artifact@v4
40
+ with:
41
+ name: dist
42
+ path: dist/
43
+
44
+ - name: Publish to PyPI
45
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,25 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+
21
+ - name: Install
22
+ run: pip install -e ".[dev]"
23
+
24
+ - name: Run tests
25
+ run: pytest tests/ -v
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ *.egg
10
+ *.yoromaps
11
+ *.osm.pbf
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Guindo / Altius Academy SNC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: yoro-maps
3
+ Version: 0.1.0
4
+ Summary: Offline maps, routing, and POI for Africa — companion to yoro geocoding
5
+ Project-URL: Homepage, https://github.com/Altius-Academy-SNC/yoro-maps
6
+ Project-URL: Repository, https://github.com/Altius-Academy-SNC/yoro-maps
7
+ Author-email: Paul Guindo <paulguindo@altius-group.ch>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: africa,maps,mbtiles,offline,osm,routing
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: Django
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: GIS
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.24
19
+ Requires-Dist: yoro>=0.2.0
20
+ Provides-Extra: all
21
+ Requires-Dist: django>=4.2; extra == 'all'
22
+ Requires-Dist: mercantile>=1.2; extra == 'all'
23
+ Requires-Dist: pyosmium>=3.6; extra == 'all'
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Requires-Dist: pytest>=7; extra == 'dev'
27
+ Requires-Dist: ruff; extra == 'dev'
28
+ Provides-Extra: django
29
+ Requires-Dist: django>=4.2; extra == 'django'
30
+ Provides-Extra: extract
31
+ Requires-Dist: pyosmium>=3.6; extra == 'extract'
32
+ Provides-Extra: tiles
33
+ Requires-Dist: mercantile>=1.2; extra == 'tiles'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Yoro Maps
37
+
38
+ Offline maps, routing, and POI for Africa — companion to [yoro](https://pypi.org/project/yoro/) geocoding.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install yoro-maps
44
+ pip install yoro-maps[django] # Django integration
45
+ pip install yoro-maps[tiles] # Tile downloading
46
+ ```
47
+
48
+ ## Build a map database
49
+
50
+ ```bash
51
+ # Download OSM data and build road graph for Mali
52
+ yoromaps download ML --output mali.yoromaps
53
+
54
+ # With map tiles (slower, ~200+ MB)
55
+ yoromaps download ML --output mali.yoromaps --tiles --zoom-max 14
56
+ ```
57
+
58
+ ## Route between Yoro codes
59
+
60
+ ```bash
61
+ yoromaps route mali.yoromaps ML-ABC ML-XYZ
62
+ ```
63
+
64
+ ```python
65
+ import yoromaps
66
+
67
+ conn = yoromaps.open_db("mali.yoromaps")
68
+ result = yoromaps.route(conn, start_lat=12.6, start_lon=-8.0, end_lat=14.5, end_lon=-4.0)
69
+ print(f"{result.distance_km} km, {result.duration_min} min")
70
+ ```
71
+
72
+ ## Django integration
73
+
74
+ ```python
75
+ # settings.py
76
+ import yoromaps
77
+
78
+ DATABASES = {
79
+ "default": { ... },
80
+ "maps": yoromaps.db_config("/data/mali.yoromaps"),
81
+ }
82
+
83
+ DATABASE_ROUTERS = ["yoromaps.django.router.YoroMapsRouter"]
84
+
85
+ # urls.py
86
+ urlpatterns = [
87
+ path("maps/", include("yoromaps.django.urls")),
88
+ ]
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT — Paul Guindo / Altius Academy SNC.
@@ -0,0 +1,58 @@
1
+ # Yoro Maps
2
+
3
+ Offline maps, routing, and POI for Africa — companion to [yoro](https://pypi.org/project/yoro/) geocoding.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install yoro-maps
9
+ pip install yoro-maps[django] # Django integration
10
+ pip install yoro-maps[tiles] # Tile downloading
11
+ ```
12
+
13
+ ## Build a map database
14
+
15
+ ```bash
16
+ # Download OSM data and build road graph for Mali
17
+ yoromaps download ML --output mali.yoromaps
18
+
19
+ # With map tiles (slower, ~200+ MB)
20
+ yoromaps download ML --output mali.yoromaps --tiles --zoom-max 14
21
+ ```
22
+
23
+ ## Route between Yoro codes
24
+
25
+ ```bash
26
+ yoromaps route mali.yoromaps ML-ABC ML-XYZ
27
+ ```
28
+
29
+ ```python
30
+ import yoromaps
31
+
32
+ conn = yoromaps.open_db("mali.yoromaps")
33
+ result = yoromaps.route(conn, start_lat=12.6, start_lon=-8.0, end_lat=14.5, end_lon=-4.0)
34
+ print(f"{result.distance_km} km, {result.duration_min} min")
35
+ ```
36
+
37
+ ## Django integration
38
+
39
+ ```python
40
+ # settings.py
41
+ import yoromaps
42
+
43
+ DATABASES = {
44
+ "default": { ... },
45
+ "maps": yoromaps.db_config("/data/mali.yoromaps"),
46
+ }
47
+
48
+ DATABASE_ROUTERS = ["yoromaps.django.router.YoroMapsRouter"]
49
+
50
+ # urls.py
51
+ urlpatterns = [
52
+ path("maps/", include("yoromaps.django.urls")),
53
+ ]
54
+ ```
55
+
56
+ ## License
57
+
58
+ MIT — Paul Guindo / Altius Academy SNC.
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "yoro-maps"
7
+ version = "0.1.0"
8
+ description = "Offline maps, routing, and POI for Africa — companion to yoro geocoding"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Paul Guindo", email = "paulguindo@altius-group.ch" },
14
+ ]
15
+ keywords = ["maps", "routing", "offline", "africa", "osm", "mbtiles"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Scientific/Engineering :: GIS",
22
+ "Framework :: Django",
23
+ ]
24
+ dependencies = [
25
+ "yoro>=0.2.0",
26
+ "httpx>=0.24",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ django = ["Django>=4.2"]
31
+ tiles = ["mercantile>=1.2"]
32
+ extract = ["pyosmium>=3.6"]
33
+ all = ["yoro-maps[django,tiles,extract]"]
34
+ dev = [
35
+ "pytest>=7",
36
+ "pytest-cov",
37
+ "ruff",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/Altius-Academy-SNC/yoro-maps"
42
+ Repository = "https://github.com/Altius-Academy-SNC/yoro-maps"
43
+
44
+ [project.scripts]
45
+ yoromaps = "yoromaps.cli:main"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/yoromaps"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ pythonpath = ["src"]
53
+
54
+ [tool.ruff]
55
+ target-version = "py310"
56
+ line-length = 100
@@ -0,0 +1,36 @@
1
+ """
2
+ Yoro Maps — Offline maps, routing, and POI for Africa.
3
+
4
+ Companion to the ``yoro`` geocoding package.
5
+
6
+ Usage::
7
+
8
+ import yoromaps
9
+
10
+ # Build a .yoromaps file for Mali
11
+ yoromaps.build("ML", "mali.yoromaps")
12
+
13
+ # Route between Yoro codes
14
+ conn = yoromaps.open_db("mali.yoromaps")
15
+ result = yoromaps.route(conn, start_lat=12.6, start_lon=-8.0, end_lat=14.5, end_lon=-4.0)
16
+ print(f"{result.distance_km} km, {result.duration_min} min")
17
+
18
+ # Route from Yoro codes
19
+ legs = yoromaps.route_from_codes(conn, ["ML-ABC", "ML-XYZ"])
20
+ """
21
+
22
+ from yoromaps.db import db_config, open_db
23
+ from yoromaps.download import build
24
+ from yoromaps.routing import RouteResult, route, route_from_codes
25
+
26
+ __version__ = "0.1.0"
27
+
28
+ __all__ = [
29
+ "build",
30
+ "open_db",
31
+ "db_config",
32
+ "route",
33
+ "route_from_codes",
34
+ "RouteResult",
35
+ "__version__",
36
+ ]
@@ -0,0 +1,139 @@
1
+ """
2
+ CLI for yoro-maps.
3
+
4
+ Usage::
5
+
6
+ yoromaps download ML --output mali.yoromaps
7
+ yoromaps download ML --output mali.yoromaps --tiles --zoom-max 14
8
+ yoromaps route mali.yoromaps ML-ABC ML-XYZ
9
+ yoromaps info mali.yoromaps
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+
18
+ from yoromaps.countries import COUNTRIES
19
+
20
+
21
+ def _progress(msg, current, total):
22
+ if total > 0:
23
+ pct = current / total * 100
24
+ print(f"\r {msg}: {pct:.0f}% ({current}/{total})", end="", flush=True)
25
+ else:
26
+ print(f"\r {msg}", end="", flush=True)
27
+
28
+
29
+ def _download(args):
30
+ from yoromaps.download import build
31
+
32
+ code = args.country.upper()
33
+ if code not in COUNTRIES:
34
+ print(f"Unknown country: {code}. Available: {', '.join(sorted(COUNTRIES))}", file=sys.stderr)
35
+ sys.exit(1)
36
+
37
+ output = args.output or f"{code.lower()}.yoromaps"
38
+ print(f"Building {output} for {COUNTRIES[code].name}...")
39
+
40
+ build(
41
+ code,
42
+ output=output,
43
+ pbf_path=args.pbf,
44
+ include_tiles=args.tiles,
45
+ zoom_min=args.zoom_min,
46
+ zoom_max=args.zoom_max,
47
+ progress=_progress,
48
+ )
49
+ print(f"\nDone: {output}")
50
+
51
+
52
+ def _route(args):
53
+ from yoromaps.db import open_db
54
+ from yoromaps.routing import route_from_codes
55
+
56
+ conn = open_db(args.db)
57
+ legs = route_from_codes(conn, args.codes)
58
+ conn.close()
59
+
60
+ total_km = sum(leg.distance_km for leg in legs)
61
+ total_min = sum(leg.duration_min for leg in legs)
62
+
63
+ result = {
64
+ "total_distance_km": total_km,
65
+ "total_duration_min": total_min,
66
+ "legs": [
67
+ {
68
+ "distance_km": leg.distance_km,
69
+ "duration_min": leg.duration_min,
70
+ "found": leg.found,
71
+ "steps": leg.steps,
72
+ }
73
+ for leg in legs
74
+ ],
75
+ }
76
+ print(json.dumps(result, indent=2))
77
+
78
+
79
+ def _info(args):
80
+ from yoromaps.db import open_db, get_metadata
81
+ from yoromaps.tiles import tile_count
82
+
83
+ conn = open_db(args.db)
84
+ print(f"Database: {args.db}")
85
+ for key in ("country", "country_name", "bbox", "graph_nodes", "graph_edges", "tiles_count"):
86
+ val = get_metadata(conn, key)
87
+ if val:
88
+ print(f" {key}: {val}")
89
+ tc = tile_count(conn)
90
+ if tc:
91
+ print(f" tiles_in_db: {tc}")
92
+ conn.close()
93
+
94
+
95
+ def _countries(args):
96
+ for code, c in sorted(COUNTRIES.items()):
97
+ print(f" {code} {c.name}")
98
+
99
+
100
+ def main():
101
+ parser = argparse.ArgumentParser(prog="yoromaps", description="Offline maps & routing for Africa")
102
+ sub = parser.add_subparsers(dest="command")
103
+
104
+ # download
105
+ dl = sub.add_parser("download", help="Download and build a .yoromaps file")
106
+ dl.add_argument("country", help="ISO country code (e.g. ML, BF, CI)")
107
+ dl.add_argument("--output", "-o", help="Output file path")
108
+ dl.add_argument("--pbf", help="Use existing PBF file instead of downloading")
109
+ dl.add_argument("--tiles", action="store_true", help="Also download map tiles")
110
+ dl.add_argument("--zoom-min", type=int, default=6)
111
+ dl.add_argument("--zoom-max", type=int, default=12)
112
+
113
+ # route
114
+ rt = sub.add_parser("route", help="Calculate route between Yoro codes")
115
+ rt.add_argument("db", help="Path to .yoromaps file")
116
+ rt.add_argument("codes", nargs="+", help="Yoro codes (e.g. ML-ABC ML-XYZ)")
117
+
118
+ # info
119
+ nf = sub.add_parser("info", help="Show database info")
120
+ nf.add_argument("db", help="Path to .yoromaps file")
121
+
122
+ # countries
123
+ sub.add_parser("countries", help="List available countries")
124
+
125
+ args = parser.parse_args()
126
+ if args.command == "download":
127
+ _download(args)
128
+ elif args.command == "route":
129
+ _route(args)
130
+ elif args.command == "info":
131
+ _info(args)
132
+ elif args.command == "countries":
133
+ _countries(args)
134
+ else:
135
+ parser.print_help()
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
@@ -0,0 +1,54 @@
1
+ """
2
+ Country registry — Geofabrik download URLs and metadata.
3
+
4
+ Focused on Africa, extensible to other regions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class Country:
14
+ code: str
15
+ name: str
16
+ geofabrik_path: str
17
+ bbox: tuple[float, float, float, float] # lon_min, lat_min, lon_max, lat_max
18
+
19
+
20
+ COUNTRIES: dict[str, Country] = {
21
+ # --- West Africa (priority) ---
22
+ "ML": Country("ML", "Mali", "africa/mali", (-12.5, 10.0, 4.5, 25.0)),
23
+ "NE": Country("NE", "Niger", "africa/niger", (0.0, 11.5, 16.0, 24.0)),
24
+ "BF": Country("BF", "Burkina Faso", "africa/burkina-faso", (-5.5, 9.0, 2.5, 15.5)),
25
+ "CI": Country("CI", "Cote d'Ivoire", "africa/ivory-coast", (-8.6, 4.36, -2.49, 10.74)),
26
+ "GH": Country("GH", "Ghana", "africa/ghana", (-3.26, 4.74, 1.2, 11.17)),
27
+ "SN": Country("SN", "Senegal", "africa/senegal-and-gambia", (-17.54, 12.31, -11.36, 16.69)),
28
+ "GN": Country("GN", "Guinee", "africa/guinea", (-15.08, 7.19, -7.64, 12.68)),
29
+ "TG": Country("TG", "Togo", "africa/togo", (-0.15, 6.1, 1.81, 11.14)),
30
+ "BJ": Country("BJ", "Benin", "africa/benin", (0.77, 6.14, 3.85, 12.41)),
31
+ "NG": Country("NG", "Nigeria", "africa/nigeria", (2.69, 4.27, 14.68, 13.89)),
32
+ "CM": Country("CM", "Cameroun", "africa/cameroon", (8.49, 1.65, 16.19, 13.08)),
33
+ "SL": Country("SL", "Sierra Leone", "africa/sierra-leone", (-13.3, 6.93, -10.27, 10.0)),
34
+ "LR": Country("LR", "Liberia", "africa/liberia", (-11.49, 4.35, -7.37, 8.55)),
35
+ "MR": Country("MR", "Mauritanie", "africa/mauritania", (-17.07, 14.72, -4.83, 27.3)),
36
+ "GW": Country("GW", "Guinee-Bissau", "africa/guinea-bissau", (-16.71, 10.87, -13.64, 12.68)),
37
+ # --- Central / East Africa ---
38
+ "CD": Country("CD", "RD Congo", "africa/congo-democratic-republic", (12.18, -13.46, 31.31, 5.39)),
39
+ "KE": Country("KE", "Kenya", "africa/kenya", (33.91, -4.68, 41.91, 5.03)),
40
+ "TZ": Country("TZ", "Tanzanie", "africa/tanzania", (29.33, -11.75, 40.44, -0.99)),
41
+ "UG": Country("UG", "Ouganda", "africa/uganda", (29.57, -1.48, 35.04, 4.23)),
42
+ "ET": Country("ET", "Ethiopie", "africa/ethiopia", (32.99, 3.4, 47.99, 14.89)),
43
+ "MG": Country("MG", "Madagascar", "africa/madagascar", (43.18, -25.61, 50.48, -11.95)),
44
+ }
45
+
46
+ GEOFABRIK_BASE = "https://download.geofabrik.de"
47
+
48
+
49
+ def geofabrik_url(country_code: str) -> str:
50
+ """Return the Geofabrik PBF download URL for a country."""
51
+ c = COUNTRIES.get(country_code.upper())
52
+ if not c:
53
+ raise ValueError(f"Unknown country: {country_code}. Available: {list(COUNTRIES.keys())}")
54
+ return f"{GEOFABRIK_BASE}/{c.geofabrik_path}-latest.osm.pbf"
@@ -0,0 +1,112 @@
1
+ """
2
+ Database management for .yoromaps files.
3
+
4
+ A .yoromaps file is a single SQLite database containing:
5
+ - tiles: MBTiles-compatible tile storage
6
+ - nodes: road graph intersections
7
+ - edges: road graph segments
8
+ - pois: points of interest
9
+ - metadata: version, country, timestamps
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import sqlite3
15
+ from pathlib import Path
16
+
17
+ SCHEMA_VERSION = 1
18
+
19
+ SCHEMA_SQL = """
20
+ -- Metadata
21
+ CREATE TABLE IF NOT EXISTS metadata (
22
+ key TEXT PRIMARY KEY,
23
+ value TEXT
24
+ );
25
+
26
+ -- MBTiles-compatible tile storage
27
+ CREATE TABLE IF NOT EXISTS tiles (
28
+ zoom_level INTEGER NOT NULL,
29
+ tile_column INTEGER NOT NULL,
30
+ tile_row INTEGER NOT NULL,
31
+ tile_data BLOB NOT NULL,
32
+ PRIMARY KEY (zoom_level, tile_column, tile_row)
33
+ );
34
+
35
+ -- Road graph: nodes (intersections)
36
+ CREATE TABLE IF NOT EXISTS nodes (
37
+ id INTEGER PRIMARY KEY,
38
+ lat REAL NOT NULL,
39
+ lon REAL NOT NULL
40
+ );
41
+
42
+ -- Road graph: edges (road segments)
43
+ CREATE TABLE IF NOT EXISTS edges (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ from_node INTEGER NOT NULL REFERENCES nodes(id),
46
+ to_node INTEGER NOT NULL REFERENCES nodes(id),
47
+ distance_m REAL NOT NULL,
48
+ duration_s REAL NOT NULL,
49
+ road_type TEXT NOT NULL DEFAULT 'road',
50
+ oneway INTEGER NOT NULL DEFAULT 0,
51
+ name TEXT DEFAULT ''
52
+ );
53
+ CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_node);
54
+ CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_node);
55
+
56
+ -- Points of interest
57
+ CREATE TABLE IF NOT EXISTS pois (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ lat REAL NOT NULL,
60
+ lon REAL NOT NULL,
61
+ yoro_code TEXT NOT NULL DEFAULT '',
62
+ name TEXT NOT NULL,
63
+ category TEXT NOT NULL DEFAULT 'other',
64
+ source TEXT NOT NULL DEFAULT 'local',
65
+ osm_id INTEGER,
66
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
67
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
68
+ );
69
+ CREATE INDEX IF NOT EXISTS idx_pois_yoro ON pois(yoro_code);
70
+ CREATE INDEX IF NOT EXISTS idx_pois_category ON pois(category);
71
+ """
72
+
73
+
74
+ def open_db(path: str | Path, create: bool = False) -> sqlite3.Connection:
75
+ """Open a .yoromaps database. Creates schema if *create* is True."""
76
+ path = Path(path)
77
+ exists = path.exists()
78
+
79
+ if not exists and not create:
80
+ raise FileNotFoundError(f"Database not found: {path}")
81
+
82
+ conn = sqlite3.connect(str(path))
83
+ conn.row_factory = sqlite3.Row
84
+ conn.execute("PRAGMA journal_mode=WAL")
85
+ conn.execute("PRAGMA synchronous=NORMAL")
86
+ conn.execute("PRAGMA foreign_keys=ON")
87
+
88
+ if create and not exists:
89
+ conn.executescript(SCHEMA_SQL)
90
+ conn.execute("INSERT OR REPLACE INTO metadata VALUES ('schema_version', ?)",
91
+ (str(SCHEMA_VERSION),))
92
+ conn.commit()
93
+
94
+ return conn
95
+
96
+
97
+ def set_metadata(conn: sqlite3.Connection, key: str, value: str) -> None:
98
+ conn.execute("INSERT OR REPLACE INTO metadata VALUES (?, ?)", (key, value))
99
+ conn.commit()
100
+
101
+
102
+ def get_metadata(conn: sqlite3.Connection, key: str) -> str | None:
103
+ row = conn.execute("SELECT value FROM metadata WHERE key = ?", (key,)).fetchone()
104
+ return row[0] if row else None
105
+
106
+
107
+ def db_config(path: str) -> dict:
108
+ """Return a Django DATABASES dict entry for a .yoromaps file."""
109
+ return {
110
+ "ENGINE": "django.db.backends.sqlite3",
111
+ "NAME": str(path),
112
+ }
@@ -0,0 +1,18 @@
1
+ """
2
+ Yoro Maps Django integration.
3
+
4
+ Add to your settings::
5
+
6
+ import yoromaps
7
+
8
+ INSTALLED_APPS = [..., "yoromaps.django"]
9
+
10
+ DATABASES = {
11
+ "default": { ... },
12
+ "maps": yoromaps.db_config("/path/to/country.yoromaps"),
13
+ }
14
+
15
+ DATABASE_ROUTERS = ["yoromaps.django.router.YoroMapsRouter"]
16
+ """
17
+
18
+ default_app_config = "yoromaps.django.apps.YoroMapsConfig"
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class YoroMapsConfig(AppConfig):
5
+ name = "yoromaps.django"
6
+ label = "yoromaps"
7
+ verbose_name = "Yoro Maps"
8
+ default_auto_field = "django.db.models.BigAutoField"