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. The search minimises time,
and the speed of every edge comes from profiles at query time — so one loaded
graph answers for a car, a moto, a bicycle and a pedestrian.
Graph.routes offers several ways of getting there rather than one: the
quickest, then the best remaining answers once the streets already used are
made dearer, keeping only those that share less than MAX_OVERLAP of their
length with one already offered. Fewer come back when there are genuinely
fewer — a town with one bridge has one way out.
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.
ALTERNATIVE_PENALTY = 3.0
module-attribute
¶
MAX_OVERLAP = 0.7
module-attribute
¶
MAX_ALTERNATIVE_ATTEMPTS = 6
module-attribute
¶
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
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 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 | |
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, accept=None)
¶
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.
accept narrows what counts as a node. Without it this answers "what
is the nearest piece of road", which is the wrong question for a
router: see _snap.
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. It changes
where the journey is joined, too — see _snap.
Source code in src/yoromaps/routing.py
routes(start_lat, start_lon, end_lat, end_lon, mode='car', count=3)
¶
The quickest route, then up to count - 1 genuinely different ones.
Sorted by how long they take, quickest first. Not by the order they were found, which is a different order and was briefly mistaken for this one: the second pass minimises a penalised cost, so the third candidate can be genuinely quicker than the second. Measured on Mali's own bundle it was — 12.2 minutes offered below 12.5 — and a list whose second entry is slower than its third is a list the reader has to check rather than read.
The method is penalty and re-search: trace the best route, triple the
cost of the streets it used, ask again, and keep the answer only if it
shares less than MAX_OVERLAP of its length with one already offered.
Two routes down the same road with a different turn at the end are one
route, and offering them as two wastes the reader's attention on the
one decision they do not have to make.
Fewer than count come back when there are genuinely fewer ways —
a town with one bridge has one route out, and inventing a second would
be inventing a road. One is a complete answer; none means the same as
route() returning found=False.
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
routes_between_codes(conn, from_code, to_code, graph=None, mode='car', count=3)
¶
Several ways from one Yoro code to another, quickest first.
One leg only, unlike route_from_codes. Alternatives through a list of
waypoints multiply out — three ways to each of four stops is eighty-one
journeys — and nobody asked to choose between eighty-one.
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 = 5
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.
A road is more than its type, and three of its tags change the answer:
- Surface. A
primaryin laterite is not aprimaryin asphalt, and outside the capitals most of them are laterite: 143,000 of Mali's 361,000 ways sayunpaved. The surface is stored on the edge and caps the speed here, at query time, like everything else in this file. - Access.
access=private,motor_vehicle=noand the rest close a way to some modes and not others. Resolving OSM's tag hierarchy needs every tag of the way, so it is done once, at extraction, and the edge stores the modes it is closed to (edges.closed_to). - One-way. It used to be baked into the graph's shape — a one-way was
written as one row, so a pedestrian could not be allowed up it: there was
nothing to allow. Both directions are now written, and the one against the
flow is closed to the modes in
ONEWAY_BOUND.
Known limit, requiring a change to the bundle rather than to this file:
- 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, surface='')
¶
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.