diff --git a/openpilot/starpilot/galaxy/web/js/navigation.js b/openpilot/starpilot/galaxy/web/js/navigation.js index c36d66b4e1..e28ad4ccef 100644 --- a/openpilot/starpilot/galaxy/web/js/navigation.js +++ b/openpilot/starpilot/galaxy/web/js/navigation.js @@ -534,10 +534,10 @@ export const NavigationPage = {
Mapbox

One public Mapbox access token (starting with pk.) handles the map, place and address search, and routes. A separate secret token is not needed.

-

Save your token while parked. Galaxy keeps it on your comma and never displays it after saving. Permanent address results can be saved. Search suggestions are temporary and used only for the current route. Favorites and recent destinations are also kept on your comma; going to one skips search, so it uses none of your search allowance. URL-restricted keys may reject map requests.

-

Typing suggestions and offline road maps stop just short of Mapbox's free monthly allowance and resume on the 1st. Permanent address lookups are billed by Mapbox. Searches, address lookups, routes and map views are counted here but not limited.

+

Save your token while parked. Galaxy keeps it on your comma and never displays it after saving. Address searches use temporary geocoding. Search suggestions are temporary and used only for the current route. Favorites and recent destinations are also kept on your comma; going to one skips search, so it uses none of your search allowance. URL-restricted keys may reject map requests.

+

Typing suggestions, address lookups and offline road maps stop just short of their free monthly allowances and resume on the 1st. Routes, full place searches and map views are counted but not limited. These counts cover this comma only; Mapbox bills total account usage, including other apps and devices.

- +
Get a Mapbox access token

A Mapbox key is saved.

diff --git a/openpilot/starpilot/navigation/mapbox_budget.py b/openpilot/starpilot/navigation/mapbox_budget.py index d05b804ef4..b01ce03e44 100644 --- a/openpilot/starpilot/navigation/mapbox_budget.py +++ b/openpilot/starpilot/navigation/mapbox_budget.py @@ -20,7 +20,7 @@ from pathlib import Path # Free monthly allowances (mapbox.com/pricing, 2026-10). FREE = { "searchSessions": 500, # Search Box API sessions (typing and searching) - "geocoding": 0, # Permanent address results have no free tier; counted, not capped + "geocoding": 100_000, # Temporary Geocoding API requests "staticTiles": 200_000, # Static Tiles API (Galaxy's map pictures) "directions": 100_000, # Directions API (routes) "vectorTiles": 200_000, # Vector Tiles API (offline road maps) diff --git a/openpilot/starpilot/navigation/owner.py b/openpilot/starpilot/navigation/owner.py index ee3b446fa4..cbd077cba5 100644 --- a/openpilot/starpilot/navigation/owner.py +++ b/openpilot/starpilot/navigation/owner.py @@ -513,10 +513,13 @@ class NavigationOwner: token = self.read()['token'] if not token: raise ValidationError('Add your Mapbox access token first') - # Address results are saved in favorites and recents, so request permanent geocoding. - self.geocode_budget.spend(enforce=False) + from openpilot.starpilot.navigation.mapbox_budget import BudgetExhausted + try: + self.geocode_budget.spend() + except BudgetExhausted as error: + raise ValidationError(str(error)) from None data = response_json(self.session, 'https://api.mapbox.com/search/geocode/v6/forward', - {'q': query.strip(), 'access_token': token, 'limit': 8, 'autocomplete': 'false', 'permanent': 'true', **self._search_context()}) + {'q': query.strip(), 'access_token': token, 'limit': 8, 'autocomplete': 'false', 'permanent': 'false', **self._search_context()}) results = [] features = data.get('features') if not isinstance(features, list): diff --git a/openpilot/starpilot/navigation/tests/test_search_places.py b/openpilot/starpilot/navigation/tests/test_search_places.py index 5871bd7c7b..ce4be065da 100644 --- a/openpilot/starpilot/navigation/tests/test_search_places.py +++ b/openpilot/starpilot/navigation/tests/test_search_places.py @@ -112,10 +112,41 @@ def test_suggestions_cannot_be_saved_but_remain_choosable(owner): assert choose(owner, result)[0]['destination']['temporary'] is True -def test_address_search_requests_permanent_results(owner): +def test_address_search_requests_only_temporary_results(owner): with patch('openpilot.starpilot.navigation.owner.response_json', return_value={'features': []}) as provider: assert owner.search('100 Main Street') == [] - assert provider.call_args.args[2]['permanent'] == 'true' + request = provider.call_args.args[2] + assert request['permanent'] == 'false' + assert request['autocomplete'] == 'false' + assert owner.geocode_budget.used == 1 + + +def test_address_search_stops_before_temporary_free_allowance(owner): + assert owner.geocode_budget.limit == 99_000 + owner.geocode_budget.used = owner.geocode_budget.limit + with patch('openpilot.starpilot.navigation.owner.response_json') as provider: + with pytest.raises(ValidationError, match='free Mapbox address lookups'): + owner.search('100 Main Street') + provider.assert_not_called() + + +def test_place_fallback_never_requests_permanent_geocoding(owner): + with patch('openpilot.starpilot.navigation.owner.response_json', side_effect=[{'suggestions': []}, {'features': []}]) as provider: + assert owner.search_places('100 Main Street', 'caller', str(uuid.uuid4()), str(uuid.uuid4())) == [] + assert len(provider.call_args_list) == 2 + assert provider.call_args_list[-1].args[2]['permanent'] == 'false' + + +def test_home_work_remain_on_device_after_restart_without_search(owner): + revision = owner.read()['revision'] + for label, place in (('home', ADDRESS), ('work', {'name': 'Work', 'latitude': 40., 'longitude': -90.})): + revision = owner.favorite(place, revision, True, label=label)['revision'] + with patch('openpilot.starpilot.navigation.owner.response_json', side_effect=AssertionError('saved destinations must not search')): + restored = NavigationOwner(owner.root, runtime_source=lambda: None, transient_root=owner.transient_root) + favorites = restored.read()['favorites'] + assert {place['label'] for place in favorites} == {'home', 'work'} + selected = restored.select(favorites[0], restored.read()['revision'], True) + assert selected['destination']['name'] == favorites[0]['name'] def test_temporary_destination_supports_full_unicode_address(owner): diff --git a/prebuilt.json b/prebuilt.json index b670068911..4c5a1ed4bc 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "815653e909618674ddde59f3c7f71369cea3f42cb1b6f7cc31f27c382b8bdcbb", "schema_version": 1} +{"protected_inventory_sha256": "378066f3a281baa3caf81e18c5be50ed24996b2623e5fac1b3a1114a3fdc2fee", "schema_version": 1}