navi/backend/services/navi_offroute/app.py
Matt 070c0d4d88 MVUM Layer 3b: OSM parking as multi-modal Auto transition candidates
Adds OSM parking lots as a third multi-modal-Auto transition source alongside
MVUM trailheads (3a) and surface-change points (3c), so Auto can suggest
"drive to a parking lot, switch to foot/2w/4w" trips where no MVUM trailhead
exists -- BLM/state land, urban edges, anywhere OSM has parking but the USFS
trailhead layer does not. Backend-only; consumes the already-ingested
/mnt/nav/osm-parking.db read-only (no data-pipeline change).

- mvum_parking.py: OSMParkingIndex (process-wide singleton via load_parking_index)
  over a shapely STRtree of parking points, mirroring MVUMSpatialIndex /
  TrailheadIndex. Read-only SQLite. Drops access in (private,no,permit) at load.
  query_parking_near_line(coords, buffer_m=2000) with the same coarse-bbox +
  precise-distance filter as TrailheadIndex. Records carry
  {lat, lon, name, road_class="parking", parking_type, access}.
  Perf note: the ingest already stored representative_point() in lat/lon, so the
  STRtree is built straight from those columns -- parsing the 1.6M WKB blobs at
  boot would add minutes for an identical point.
- router.py: _try_hybrid_auto generalized to gather candidates from each AVAILABLE
  source (trailhead index if present + surface-change always + parking index if
  present) instead of hard-returning when trailhead_index is None, so parking-only
  candidates still work. Combined list keeps the existing closest-first sort +
  HYBRID_MAX_TRAILHEADS cap. Signature unchanged; record shape already compatible.
- app.py / offroute_route.py: load + inject the OSM parking singleton, mirroring
  MVUM_SPATIAL_INDEX / MVUM_TRAILHEAD_INDEX. Failure logs a warning, degrades None.
- admin.py: GET /api/admin/osm-parking/info -> {count, build_time_seconds,
  memory_estimate_mb}, mirroring /api/admin/mvum-spatial/info.
- backend/scripts/ingest_parking.py + README-osm-parking-ingest.md: the
  data-pipeline ingest lifted to the repo with argparse (--geojsonseq/--db, no
  /tmp) + the download/filter/export/ingest/restart refresh recipe.

Tests: test_mvum_parking.py (loads, near-line close-only, private/no/permit
filtered, null-access kept) + test_offroute.py::test_hybrid_consumes_parking_
candidates (parking-only source probed as a leg-1 destination). Full offroute
suite: 82 passed.

Real-DB sanity (not deployed): index loads 1,489,054 usable parking objects
(182,945 access-blocked dropped) in ~12 s using ~950 MB RSS per worker; a Redfish
Lake/Sawtooth corridor query returns 8 lots. The ~950 MB/worker memory cost is
notable -- flagging for review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:15:11 +00:00

75 lines
2.3 KiB
Python

"""navi-offroute Flask application factory + gunicorn entry.
Gunicorn entry:
gunicorn 'services.navi_offroute.app:create_app()' --bind 127.0.0.1:8428 --workers 2
"""
import time
from flask import Flask
from shared.git_sha import git_short_sha
from . import offroute_route, admin
from .mvum import MVUMSpatialIndex
from .mvum_transitions import load_trailheads
from .mvum_parking import load_parking_index
# Process-wide singleton: build the MVUM spatial index once per process (per gunicorn
# worker in prod; once across create_app() calls in tests), not once per app instance.
_MVUM_INDEX = None
def _get_mvum_index():
global _MVUM_INDEX
if _MVUM_INDEX is None:
_MVUM_INDEX = MVUMSpatialIndex()
return _MVUM_INDEX
def create_app():
app = Flask(__name__)
app.config['VERSION'] = git_short_sha()
app.config['METRICS'] = {
'start_time': time.time(),
'request_count': 0,
'last_error_at': None,
}
@app.before_request
def _count_request():
app.config['METRICS']['request_count'] += 1
@app.after_request
def _track_errors(response):
if response.status_code >= 500:
app.config['METRICS']['last_error_at'] = time.strftime(
'%Y-%m-%dT%H:%M:%SZ', time.gmtime()
)
return response
# Load the MVUM spatial index once at service init (logs its own load line).
try:
app.config['MVUM_SPATIAL_INDEX'] = _get_mvum_index()
except Exception as e:
app.logger.warning("MVUM spatial index failed to load: %s", e)
app.config['MVUM_SPATIAL_INDEX'] = None
# Layer 3a: trailhead transition index (process-wide singleton, logs its own line).
try:
app.config['MVUM_TRAILHEAD_INDEX'] = load_trailheads()
except Exception as e:
app.logger.warning("MVUM trailhead index failed to load: %s", e)
app.config['MVUM_TRAILHEAD_INDEX'] = None
# Layer 3b: OSM parking index (process-wide singleton, logs its own line).
try:
app.config['OSM_PARKING_INDEX'] = load_parking_index()
except Exception as e:
app.logger.warning("OSM parking index failed to load: %s", e)
app.config['OSM_PARKING_INDEX'] = None
app.register_blueprint(offroute_route.bp)
app.register_blueprint(admin.bp)
return app