Core API Reference¶
yoromaps¶
Top-level module with convenience imports.
yoromaps
¶
Yoro Maps — Offline maps, routing, and POI, worldwide.
Companion to the yoro geocoding package.
Usage::
import yoromaps
# Build a .yoromaps file for Mali
yoromaps.build("ML", "mali.yoromaps")
# Route between Yoro codes
conn = yoromaps.open_db("mali.yoromaps")
result = yoromaps.route(conn, start_lat=12.6, start_lon=-8.0, end_lat=14.5, end_lon=-4.0)
print(f"{result.distance_km} km, {result.duration_min} min")
# Route from Yoro codes
legs = yoromaps.route_from_codes(conn, ["ML-ABC", "ML-XYZ"])
# As something other than a car. One of yoromaps.MODES; the mode changes
# the minutes *and* the roads the line runs along, since it is a
# permission as well as a speed.
legs = yoromaps.route_from_codes(conn, ["ML-ABC", "ML-XYZ"], mode="motorcycle")
RouteResult
dataclass
¶
Result of a routing query.
Source code in src/yoromaps/routing.py
build(area_code, output, pbf_path=None, include_tiles=False, zoom_min=6, zoom_max=12, include_pois=True, progress=None)
¶
Build a .yoromaps file for an area.
An area is a country that Geofabrik serves whole, or one region of a country it does not — France's extract is 4 830 MB and Alsace's is 124 MB, and only one of those is something to ask somebody to download.
The bundle records the country, not the area. Codes inside Alsace are French codes, addressed against France's box: an address does not change because somebody downloaded less of the map around it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
area_code
|
str
|
An area code — "ML" for a whole country, "FR:alsace" for one region of one. |
required |
output
|
str | Path
|
Output .yoromaps file path. |
required |
pbf_path
|
str | Path | None
|
Path to an existing PBF file. If None, downloads from Geofabrik. |
None
|
include_tiles
|
bool
|
Also download map tiles (slow, ~200+ MB). |
False
|
zoom_min
|
int
|
Min tile zoom level (if include_tiles). |
6
|
zoom_max
|
int
|
Max tile zoom level (if include_tiles). |
12
|
include_pois
|
bool
|
Also extract the shops, schools and hospitals OSM knows about. On by default — a bundle without them routes to places the user has no way to find. |
True
|
progress
|
ProgressCallback | None
|
Optional callable(message, current, total). |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to the created .yoromaps file. |
Source code in src/yoromaps/download.py
update(db_path, pbf_cache_dir=None, progress=None)
¶
Update an existing .yoromaps file with the latest OSM data.
Downloads the PBF only if Geofabrik has a newer version.
Rebuilds the road graph and re-extracts the OSM POIs from the same file. POIs from any other source — the ones contributed locally — are left untouched, since only rows tagged as OSM-sourced are replaced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_path
|
str | Path
|
Path to the existing .yoromaps file. |
required |
pbf_cache_dir
|
str | Path | None
|
Directory to cache PBF files. Defaults to same dir as db. |
None
|
progress
|
ProgressCallback | None
|
Optional progress callback. |
None
|
Returns:
| Type | Description |
|---|---|
UpdateStats
|
Dict with |
Source code in src/yoromaps/download.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
open_db(path, create=False)
¶
Open a .yoromaps database. Creates schema if create is True.
Existing databases are upgraded transparently: the schema is idempotent
(CREATE ... IF NOT EXISTS), so files built by older versions gain new
tables and indexes on first open, and columns added to a table that
already existed are applied by _add_late_columns.
Source code in src/yoromaps/db.py
db_config(path)
¶
route(conn, start_lat, start_lon, end_lat, end_lon, mode='car')
¶
Find the quickest route between two GPS coordinates.
Loads the graph into memory on each call. For multiple routes,
use Graph.from_db() directly.
Source code in src/yoromaps/routing.py
route_from_codes(conn, codes, graph=None, mode='car')
¶
Route through multiple Yoro codes in order.
Returns a list of RouteResult, one per leg. Pass a preloaded graph
(e.g. from :func:get_graph) to avoid reloading it from the database —
the same graph answers for every mode, so one load serves them all.
Source code in src/yoromaps/routing.py
get_graph(db_path)
¶
Load the road graph for a .yoromaps file, cached per process.
The graph is loaded once and reused across calls (and across HTTP
requests in Django). The cache entry is invalidated when the file's
mtime changes, e.g. after yoromaps update.
Source code in src/yoromaps/routing.py
add_poi(conn, lat, lon, name, category='other', source='local', osm_id=None, details=None)
¶
Insert a POI; its Yoro code is computed automatically.
details is one argument rather than four because a caller usually knows
none of them or all of them, and four empty strings at every call site
would say nothing four times.
Source code in src/yoromaps/poi.py
pois_near(conn, lat, lon, radius_m=1000, category=None, limit=100)
¶
POIs within radius_m metres, sorted by distance.
Source code in src/yoromaps/poi.py
pois_in_cell(conn, code, category=None)
¶
All POIs inside the cell of a Yoro code (any precision).
Raises ValueError for invalid codes.
Source code in src/yoromaps/poi.py
search_pois(conn, query=None, category=None, limit=50)
¶
Search POIs by name substring and/or category.
Source code in src/yoromaps/poi.py
delete_poi(conn, poi_id)
¶
Delete a POI by id. Returns True if a row was removed.
extract_pois(pbf_path, conn, progress=None, source=OSM_SOURCE, precision=DEFAULT_PRECISION)
¶
Extract POIs from a PBF into the database, replacing previous ones.
Rows carrying source are deleted first, so a rebuild refreshes OSM data without touching POIs from anywhere else. The delete and every insert share one transaction: a failure mid-extraction leaves the previous set in place.
Rows are written in batches of POI_INSERT_BATCH rather than accumulated, so peak memory does not grow with the size of the country.
Nameless features are skipped — a pin with no label is noise on a map whose whole purpose is telling someone where to go.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pbf_path
|
str | Path
|
Path to the |
required |
conn
|
Connection
|
Open connection to a |
required |
progress
|
ProgressCallback | None
|
Optional callable(message, current, total). |
None
|
source
|
str
|
Value written to |
OSM_SOURCE
|
precision
|
int
|
Yoro precision of the stored codes. The default addresses each POI individually; lower it only if you want neighbours to share a code on purpose. |
DEFAULT_PRECISION
|
Returns:
| Type | Description |
|---|---|
PoiExtractStats
|
Counts of what was written and walked past, plus |
PoiExtractStats
|
most frequent tag values that matched nothing, as |
PoiExtractStats
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If source is empty — it selects the rows to replace, and an empty value would delete nothing while inserting unattributable rows. |
ImportError
|
If the |
Source code in src/yoromaps/osm_poi.py
categorize(tags)
¶
The category of an OSM feature, or None when it is not a POI.
categorize({"amenity": "pharmacy"}) 'pharmacy' categorize({"shop": "bakery"}) 'shop' categorize({"highway": "residential"}) is None True
Source code in src/yoromaps/osm_poi.py
yoromaps.routing¶
A* routing engine on the .yoromaps road graph.
yoromaps.routing
¶
A* routing engine on the .yoromaps road graph.
Loads the graph adjacency list into memory for fast routing. Typical memory usage: ~100-200 MB for a country like Mali or Togo.
One graph, every mode. The edge carries what kind of road it is, and the
speed comes from profiles at query time — so the same loaded graph answers
for a car, a moto, a bicycle and a pedestrian without being loaded four
times, and correcting a speed does not mean republishing a bundle.
The search minimises time, not distance. It used to minimise distance and report the time of whatever it found, which is a different answer wearing the same label: the shortest way through a city is rarely the quickest, and a bicycle and a car sent down the same road for the same reason were only ever going to disagree about how long it took.
RouteResult
dataclass
¶
Result of a routing query.
Source code in src/yoromaps/routing.py
Graph
¶
In-memory road graph loaded from a .yoromaps database.
Load once, route many times::
graph = Graph.from_db(conn)
r1 = graph.route(6.13, 1.22, 9.55, 1.18)
r2 = graph.route(6.13, 1.22, 8.98, 1.13)
Source code in src/yoromaps/routing.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 | |
from_db(conn)
classmethod
¶
Load the entire graph into memory from a .yoromaps database.
Source code in src/yoromaps/routing.py
nearest_with_distance(lat, lon)
¶
Nearest node + haversine distance in metres.
Uses a uniform grid index (built lazily on first call) and searches outward ring by ring. The distance metric matches the historical behavior: squared degree distance for the argmin, haversine for the returned distance.
Source code in src/yoromaps/routing.py
route(start_lat, start_lon, end_lat, end_lon, mode='car')
¶
Find the quickest route between two GPS coordinates using A*.
mode is one of profiles.MODES and changes both numbers and
geometry: a bicycle is not a slow car, it is barred from the roads a
car is fastest on, and it comes back by another street.
Source code in src/yoromaps/routing.py
route(conn, start_lat, start_lon, end_lat, end_lon, mode='car')
¶
Find the quickest route between two GPS coordinates.
Loads the graph into memory on each call. For multiple routes,
use Graph.from_db() directly.
Source code in src/yoromaps/routing.py
route_from_codes(conn, codes, graph=None, mode='car')
¶
Route through multiple Yoro codes in order.
Returns a list of RouteResult, one per leg. Pass a preloaded graph
(e.g. from :func:get_graph) to avoid reloading it from the database —
the same graph answers for every mode, so one load serves them all.
Source code in src/yoromaps/routing.py
get_graph(db_path)
¶
Load the road graph for a .yoromaps file, cached per process.
The graph is loaded once and reused across calls (and across HTTP
requests in Django). The cache entry is invalidated when the file's
mtime changes, e.g. after yoromaps update.
Source code in src/yoromaps/routing.py
yoromaps.poi¶
Points of interest, addressed by Yoro code. Every POI is encoded in the
database's country domain at insertion time, falling back to the worldwide
XX domain for points outside the country's bounding box.
yoromaps.poi
¶
POI management — local points of interest addressed by Yoro codes.
Every POI gets a Yoro code at insertion time (encoded in the database's
country domain, falling back to the global XX domain when the point
lies outside the country bbox). The idx_pois_yoro index makes
"what is at this address" lookups instant; radius and cell queries use
a lat/lon bbox prefilter.
Usage::
conn = yoromaps.open_db("mali.yoromaps")
poi = add_poi(conn, 12.639, -8.002, "Grand marche", category="market")
print(poi["yoro_code"]) # "ML-..."
pois_near(conn, 12.64, -8.0, radius_m=500) # sorted by distance
pois_in_cell(conn, "ML-4H7A3B")
add_poi(conn, lat, lon, name, category='other', source='local', osm_id=None, details=None)
¶
Insert a POI; its Yoro code is computed automatically.
details is one argument rather than four because a caller usually knows
none of them or all of them, and four empty strings at every call site
would say nothing four times.
Source code in src/yoromaps/poi.py
pois_near(conn, lat, lon, radius_m=1000, category=None, limit=100)
¶
POIs within radius_m metres, sorted by distance.
Source code in src/yoromaps/poi.py
pois_in_cell(conn, code, category=None)
¶
All POIs inside the cell of a Yoro code (any precision).
Raises ValueError for invalid codes.
Source code in src/yoromaps/poi.py
search_pois(conn, query=None, category=None, limit=50)
¶
Search POIs by name substring and/or category.
Source code in src/yoromaps/poi.py
delete_poi(conn, poi_id)
¶
Delete a POI by id. Returns True if a row was removed.
yoromaps.osm_poi¶
OpenStreetMap POI extraction. Categories are an explicit tag allowlist, not a
denylist: what matches nothing is counted and reported back in
stats["unmapped"] rather than guessed at. See the
Points of Interest guide.
yoromaps.osm_poi
¶
Extract points of interest from an OSM PBF into a .yoromaps database.
The road graph tells you how to get somewhere; this tells you what is there.
Shops, hospitals, schools, markets and places of worship already mapped in
OpenStreetMap are read out of the country extract, given their Yoro code, and
written to the pois table under source='osm' — which is what makes a
freshly built bundle useful offline instead of empty.
A name and a point make a dot on a map. What makes it worth opening is what the shopfront says: a number to ring, the hours it keeps, the address it goes by. Those are read too, from the tags that carry them. Most POIs have none — in Togo about one in five carries any of them — and that is the normal case, not a failure: the fields are empty, never absent.
OSM's tagging is far richer than any address book's categories, so the mapping here is an allowlist: a tag combination that is not listed is not a POI. That keeps parking bays, benches and waste baskets out of the map. What was skipped is counted and reported, so the taxonomy can grow from evidence rather than from guesses.
Categories are deliberately those of the consuming application, so a single filter spans OSM-sourced and user-contributed places alike.
DEFAULT_PRECISION = 19
module-attribute
¶
VALUE_CATEGORIES = {'amenity': {'restaurant': 'restaurant', 'fast_food': 'restaurant', 'cafe': 'restaurant', 'bar': 'restaurant', 'pub': 'restaurant', 'food_court': 'restaurant', 'ice_cream': 'restaurant', 'marketplace': 'market', 'pharmacy': 'pharmacy', 'hospital': 'hospital', 'clinic': 'hospital', 'doctors': 'hospital', 'dentist': 'hospital', 'health_post': 'hospital', 'school': 'school', 'college': 'school', 'university': 'school', 'kindergarten': 'school', 'language_school': 'school', 'driving_school': 'school', 'library': 'school', 'place_of_worship': 'place_of_worship', 'townhall': 'government', 'courthouse': 'government', 'police': 'government', 'fire_station': 'government', 'post_office': 'government', 'embassy': 'government', 'prison': 'government', 'bus_station': 'transport', 'ferry_terminal': 'transport', 'taxi': 'transport', 'car_rental': 'transport', 'bank': 'other', 'fuel': 'other', 'bureau_de_change': 'other', 'money_transfer': 'other'}, 'tourism': {'museum': 'landmark', 'attraction': 'landmark', 'viewpoint': 'landmark', 'artwork': 'landmark', 'gallery': 'landmark', 'hotel': 'other', 'guest_house': 'other', 'hostel': 'other', 'motel': 'other'}, 'railway': {'station': 'transport', 'halt': 'transport'}, 'aeroway': {'aerodrome': 'transport', 'terminal': 'transport'}, 'public_transport': {'station': 'transport'}, 'office': {'government': 'government', 'diplomatic': 'government'}}
module-attribute
¶
KEY_CATEGORIES = {'shop': 'shop', 'craft': 'shop', 'healthcare': 'hospital', 'historic': 'landmark'}
module-attribute
¶
extract_pois(pbf_path, conn, progress=None, source=OSM_SOURCE, precision=DEFAULT_PRECISION)
¶
Extract POIs from a PBF into the database, replacing previous ones.
Rows carrying source are deleted first, so a rebuild refreshes OSM data without touching POIs from anywhere else. The delete and every insert share one transaction: a failure mid-extraction leaves the previous set in place.
Rows are written in batches of POI_INSERT_BATCH rather than accumulated, so peak memory does not grow with the size of the country.
Nameless features are skipped — a pin with no label is noise on a map whose whole purpose is telling someone where to go.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pbf_path
|
str | Path
|
Path to the |
required |
conn
|
Connection
|
Open connection to a |
required |
progress
|
ProgressCallback | None
|
Optional callable(message, current, total). |
None
|
source
|
str
|
Value written to |
OSM_SOURCE
|
precision
|
int
|
Yoro precision of the stored codes. The default addresses each POI individually; lower it only if you want neighbours to share a code on purpose. |
DEFAULT_PRECISION
|
Returns:
| Type | Description |
|---|---|
PoiExtractStats
|
Counts of what was written and walked past, plus |
PoiExtractStats
|
most frequent tag values that matched nothing, as |
PoiExtractStats
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If source is empty — it selects the rows to replace, and an empty value would delete nothing while inserting unattributable rows. |
ImportError
|
If the |
Source code in src/yoromaps/osm_poi.py
categorize(tags)
¶
The category of an OSM feature, or None when it is not a POI.
categorize({"amenity": "pharmacy"}) 'pharmacy' categorize({"shop": "bakery"}) 'shop' categorize({"highway": "residential"}) is None True
Source code in src/yoromaps/osm_poi.py
poi_name(tags)
¶
yoromaps.db¶
Database management for .yoromaps files.
yoromaps.db
¶
Database management for .yoromaps files.
A .yoromaps file is a single SQLite database containing: - tiles: MBTiles-compatible tile storage - nodes: road graph intersections - edges: road graph segments - pois: points of interest - metadata: version, country, timestamps
SCHEMA_VERSION = 4
module-attribute
¶
open_db(path, create=False)
¶
Open a .yoromaps database. Creates schema if create is True.
Existing databases are upgraded transparently: the schema is idempotent
(CREATE ... IF NOT EXISTS), so files built by older versions gain new
tables and indexes on first open, and columns added to a table that
already existed are applied by _add_late_columns.
Source code in src/yoromaps/db.py
db_config(path)
¶
set_metadata(conn, key, value)
¶
yoromaps.download¶
Download OSM data and build .yoromaps files.
yoromaps.download
¶
Download OSM data and build/update a .yoromaps file for a country.
build(area_code, output, pbf_path=None, include_tiles=False, zoom_min=6, zoom_max=12, include_pois=True, progress=None)
¶
Build a .yoromaps file for an area.
An area is a country that Geofabrik serves whole, or one region of a country it does not — France's extract is 4 830 MB and Alsace's is 124 MB, and only one of those is something to ask somebody to download.
The bundle records the country, not the area. Codes inside Alsace are French codes, addressed against France's box: an address does not change because somebody downloaded less of the map around it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
area_code
|
str
|
An area code — "ML" for a whole country, "FR:alsace" for one region of one. |
required |
output
|
str | Path
|
Output .yoromaps file path. |
required |
pbf_path
|
str | Path | None
|
Path to an existing PBF file. If None, downloads from Geofabrik. |
None
|
include_tiles
|
bool
|
Also download map tiles (slow, ~200+ MB). |
False
|
zoom_min
|
int
|
Min tile zoom level (if include_tiles). |
6
|
zoom_max
|
int
|
Max tile zoom level (if include_tiles). |
12
|
include_pois
|
bool
|
Also extract the shops, schools and hospitals OSM knows about. On by default — a bundle without them routes to places the user has no way to find. |
True
|
progress
|
ProgressCallback | None
|
Optional callable(message, current, total). |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to the created .yoromaps file. |
Source code in src/yoromaps/download.py
download_pbf(area_code, output_dir, progress=None, if_newer_than=None)
¶
Download the latest OSM PBF for an area from Geofabrik.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
area_code
|
str
|
An area code — "ML", or "FR:alsace" for one region. |
required |
output_dir
|
str | Path
|
Directory to save the PBF file. |
required |
progress
|
ProgressCallback | None
|
Optional progress callback. |
None
|
if_newer_than
|
str | None
|
HTTP date string. Skips download if server file is not newer. |
None
|
Returns:
| Type | Description |
|---|---|
Path | None
|
Path to the downloaded file, or None if skipped (already up to date). |
Source code in src/yoromaps/download.py
yoromaps.tiles¶
MBTiles management — download and serve map tiles.
yoromaps.tiles
¶
MBTiles management — download and serve map tiles from SQLite.
get_tile(conn, z, x, y)
¶
Retrieve a tile from the database (XYZ scheme, converted to TMS internally).
Source code in src/yoromaps/tiles.py
tile_count(conn)
¶
download_tiles(conn, bbox, zoom_min=6, zoom_max=14, tile_url=DEFAULT_TILE_URL, progress=None)
¶
Download map tiles for a bounding box into the database.
.. warning:: The default source is the public openstreetmap.org tile server, whose usage policy discourages bulk downloading — keep zoom levels low and prefer your own tile server or a commercial provider via tile_url for large areas.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conn
|
Connection
|
Open SQLite connection. |
required |
bbox
|
tuple[float, float, float, float]
|
(lon_min, lat_min, lon_max, lat_max). |
required |
zoom_min
|
int
|
Minimum zoom level. |
6
|
zoom_max
|
int
|
Maximum zoom level. |
14
|
tile_url
|
str
|
Tile URL template with {z}, {x}, {y} placeholders. |
DEFAULT_TILE_URL
|
progress
|
ProgressCallback | None
|
Optional callable(message, current, total). |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of tiles present for the box afterwards — newly stored plus |
int
|
already held. Tiles the server would not give up are excluded, so the |
int
|
figure counts coverage rather than attempts. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If zoom_min exceeds zoom_max, or if not one tile could be fetched — a wrong URL template or a blocked address, which a zero return would have reported as a quiet success. |
Source code in src/yoromaps/tiles.py
yoromaps.countries¶
Country registry with Geofabrik download URLs.
yoromaps.countries
¶
Country registry — Geofabrik download paths.
Names and bounding boxes come from yoro.DOMAINS (the single source of
truth for domain geometry shared by both packages); this module only adds the
Geofabrik extract path for each country.
The table is generated by scripts/gen_countries.py from the index
Geofabrik publishes, because there is no rule taking an ISO code to a file
name: Geofabrik names its extracts after regions it decided on, and several
countries have no file of their own. Those are served by the smallest extract
that does contain them — the graph then carries the neighbours' roads and the
bundle's bbox stays the country's, which is what Gambia has done inside the
Senegal extract since the first version of this registry. It costs download
size, not correctness.
COUNTRIES = {code: _country(code, path) for code, path in _GEOFABRIK_PATHS.items() if code in DOMAINS}
module-attribute
¶
GEOFABRIK_BASE = 'https://download.geofabrik.de'
module-attribute
¶
Country
dataclass
¶
geofabrik_url(country_code)
¶
Return the Geofabrik PBF download URL for a country.
Source code in src/yoromaps/countries.py
yoromaps.extract¶
OSM PBF extraction into the road graph.
yoromaps.extract
¶
Extract road graph from OSM PBF into a .yoromaps SQLite database.
Uses pyosmium to parse the PBF file in a single pass, extracting highway ways and their nodes into a graph suitable for routing.
ROUTABLE_HIGHWAYS = routable_highways()
module-attribute
¶
extract_graph(pbf_path, conn, progress=None)
¶
Extract road graph from a PBF file into the database.
Reads the file twice: once for the ways that are roads and the nodes they reference, once for those nodes' coordinates. Edges are written in batches of EDGE_INSERT_BATCH so peak memory does not follow the country's size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pbf_path
|
str | Path
|
Path to the .osm.pbf file. |
required |
conn
|
Connection
|
Open SQLite connection to a .yoromaps database. |
required |
progress
|
ProgressCallback | None
|
Optional callable(message, current, total) for progress reporting. |
None
|
Returns:
| Type | Description |
|---|---|
GraphExtractStats
|
Counts of the nodes and road segments written. |
Raises:
| Type | Description |
|---|---|
ImportError
|
If the |
Source code in src/yoromaps/extract.py
haversine(lat1, lon1, lat2, lon2)
¶
Distance in meters between two GPS points.
Source code in src/yoromaps/extract.py
yoromaps.profiles¶
How fast each way of travelling goes, on each kind of road — and which roads it may not use at all. Read at query time, so a bundle gains a mode, or a corrected speed, without being rebuilt.
yoromaps.profiles
¶
How fast each way of travelling goes, on each kind of road.
Until now the graph carried one duration per edge, computed when the bundle was built, from one speed table that described a car. Every other way of getting somewhere borrowed the car's answer: a bicycle was told it would take nine minutes to cross Bamako, and was routed down the trunk road to do it.
So the speed moved out of the file and into here, where it is read at query time. Two consequences worth stating:
- A bundle does not have to be rebuilt to gain a mode, or to correct one.
The road type is already stored on every edge — the bundle always knew what
kind of road it was, it just was not asked. Every number here is therefore
a decision that can be revisited against a real map without republishing
anything, and at least one of them already has been: see
bicycle. - A mode is not only a speed, it is a permission.
Nonemeans this mode may not use that road at all: a bicycle on a motorway is not slow, it is forbidden, and a router that merely made it slow would still send somebody onto one to save four minutes.
The numbers are West African urban conditions, not European ones. A primary road in Bamako is not a primary road in Lyon: it is shared with handcarts, mopeds and pedestrians, and 70 km/h is what it does between the jams rather than an average anybody sustains. They are estimates, and they are wrong in the way every routing profile is wrong — the point is that they are wrong per mode, which is what the map has to be honest about.
Known limits, both requiring a change to the bundle rather than to this file:
- One-way streets are baked into the graph's shape. A two-way road is
written as two rows, a one-way as one, so the reverse edge simply does not
exist. A pedestrian cannot be allowed up a one-way street from here — there
is nothing to allow. Fixing that means always writing both directions and
letting the router read the
onewayflag per mode. - A bundle built before this file has no footways in it. Extraction only
ever kept the road types a car could use, so walking routes on an old
bundle follow the roads, not the paths beside them. New bundles keep them
(see
extract.ROUTABLE_HIGHWAYS); old ones stay correct, just coarser.
MODES = ('car', 'motorcycle', 'bicycle', 'foot')
module-attribute
¶
PROFILES = {'car': {'motorway': 110, 'motorway_link': 60, 'trunk': 90, 'trunk_link': 50, 'primary': 70, 'primary_link': 40, 'secondary': 60, 'secondary_link': 35, 'tertiary': 50, 'tertiary_link': 30, 'residential': 30, 'unclassified': 40, 'living_street': 20, 'service': 20, 'track': 15, 'path': None, 'footway': None, 'cycleway': None, 'pedestrian': None, 'steps': None}, 'motorcycle': {'motorway': 90, 'motorway_link': 55, 'trunk': 80, 'trunk_link': 45, 'primary': 60, 'primary_link': 38, 'secondary': 50, 'secondary_link': 32, 'tertiary': 45, 'tertiary_link': 28, 'residential': 28, 'unclassified': 35, 'living_street': 18, 'service': 18, 'track': 25, 'path': 15, 'cycleway': 18, 'footway': None, 'pedestrian': None, 'steps': None}, 'bicycle': {'motorway': None, 'motorway_link': None, 'trunk': 13, 'trunk_link': 11, 'primary': 14, 'primary_link': 12, 'secondary': 15, 'secondary_link': 12, 'tertiary': 15, 'tertiary_link': 12, 'residential': 14, 'unclassified': 13, 'living_street': 10, 'service': 10, 'track': 8, 'path': 8, 'footway': 6, 'cycleway': 16, 'pedestrian': 6, 'steps': 2}, 'foot': {'motorway': None, 'motorway_link': None, 'trunk': 4.2, 'trunk_link': 4.2, 'primary': 4.5, 'primary_link': 4.5, 'secondary': 4.5, 'secondary_link': 4.5, 'tertiary': 4.8, 'tertiary_link': 4.8, 'residential': 4.8, 'unclassified': 4.8, 'living_street': 4.8, 'service': 4.8, 'track': 4.2, 'path': 4.0, 'footway': 4.8, 'cycleway': 4.5, 'pedestrian': 4.8, 'steps': 1.5}}
module-attribute
¶
UNKNOWN_SPEED_KMH = {'car': 30, 'motorcycle': 28, 'bicycle': 12, 'foot': 4.5}
module-attribute
¶
UnknownMode
¶
Bases: ValueError
Asked to route as something this library has no profile for.
A distinct type because the two failures are answered differently: an
unknown mode is a caller's mistake and should be raised, while "no path
for this mode" is an ordinary answer and comes back as found=False.
Source code in src/yoromaps/profiles.py
speed_kmh(mode, road_type)
¶
How fast mode travels on road_type, or None if it may not.
The one function the router asks. Raises for a mode that does not exist, because routing "as a helicopter" is not a routing failure to report back to a user — it is a call that should never have been made.
Source code in src/yoromaps/profiles.py
top_speed_kmh(mode)
¶
The fastest this mode ever goes, over every road it may use.
A needs it. The heuristic has to be a lower* bound on the time still to travel, or the search stops being optimal and starts returning whatever it reached first — so remaining distance is divided by the most optimistic speed the profile allows anywhere.
Source code in src/yoromaps/profiles.py
routable_highways()
¶
Every road type some mode can use — what extraction has to keep.
Derived rather than written out again. The list used to be the car's speed table, which is why no bundle contains a footway: what a car could not drive was never extracted, so it could never be walked either.