A production-grade route planning system with advanced address resolution and interactive map manipulation, designed for operational clarity and scalable geocoding accuracy.
Core Mission:
Bridge the gap between vague, complex user input (Nested addresses, manual corrections) and precise map coordinates via a deterministic confidence-grading pipeline.
Use Case:
Given one origin and many branch addresses, which destination is closest by actual road network distance? And how confident are we in each geocoded location?
The system employs a 3-layer modular architecture with explicit separation of concerns, enabling independent testing, provider swapping, and algorithmic evolution.
Responsibility: Clean I/O abstraction, provider communication, resilience.
Key modules:
-
js/repositories/geoRepository.js:- Multi-endpoint fallback (Photon, Nominatim, OSRM).
- Automatic retry with backoff on timeout/rate-limit.
- Timeout enforcement (12 seconds per request).
- Normalized response models across providers.
- CORS-aware request headers; provider-specific header suppression.
-
js/repositories/favRepository.js:- localStorage-backed coordinate bookmarking.
getAll(),add(),remove()CRUD interface.- Lightweight persistence for repeat destinations.
Benefits:
- Swappable providers without touching application logic.
- Centralized error handling and retry policy.
- Guaranteed timeout enforcement across all endpoints.
Responsibility: Domain logic, state management, confidence pipelines.
Key modules:
-
js/services/locationService.js(Main Coordinator):- Entry point for address resolution (
resolveLocation(inputValue, mapContext)). - Orchestrates the 6-phase resolution pipeline.
- LRU caching of finalized results.
- Dependency injection of API clients for testability.
- Entry point for address resolution (
-
js/services/modules/addressUtils.js(Parsing & Normalization):- Vietnamese diacritical normalization (đ → d).
- Hierarchical address decomposition (slash notation:
56/1/2, alley notation:38 Alley 231). - Cache key generation with 4-decimal coordinate precision.
- Area anchor extraction for territory-bounded searches.
-
js/services/modules/scoringEngine.js(Confidence Grading):- Multi-dimensional heuristic scoring (house number, street, district, POI type).
- Vectorized single-pass scoring for performance.
- Territory-mismatch safety valve (prevents marker teleportation).
- Result formatters for manual, exact, and fuzzy results.
-
js/services/modules/interpolationService.js(Geometric Logic):- Haversine distance calculations in plain JavaScript (no Leaflet dependency).
- Bounding anchor discovery for numeric addresses.
- Curved path interpolation along street geometries.
- Fallback single-anchor positioning.
-
js/services/geoMath.js(Geospatial Math Primitives):- Linear interpolation between two coordinates.
- Path-following with distance offset (Alley/Ngõ precision).
- Logical sequence validation for neighbors.
- Anchor context validation.
-
js/services/routeInteractionService.js:- Waypoint lifecycle state management.
- Route manipulation (insertion, removal, reordering).
Responsibility: Rendering, user interaction, visual feedback.
Key modules:
js/main.js: App flow orchestration (input resolution, route ranking, event wiring).js/controllers/mapController.js: Leaflet rendering and map interaction.js/ui/uiManager.js: UI state coordination.js/ui/*: Modal factories, overlay management, row state, debounce utilities.
The system resolves user input through an ordered confidence waterfall:
- Inspect LRU result cache (50-entry limit) for exact match.
- Return cached result immediately if hit (RTT: < 50ms).
- Attempt manual coordinate parsing (strict: requires decimal point).
- Format:
21.0, 105.8(accepted);56/1(rejected as address, not coords). - Return manual result with
confidence: 1.0.
- Parse complex address tokens into hierarchical stack:
- Slash notation:
56/1/2 Road→["56/1/2 Road", "56/1 Road", "56 Road"] - Alley notation:
38 alley 231 Street→["38 alley 231 Street", "Alley 231 Street", "38 Street"]
- Slash notation:
- Extract area anchor (Ward/District/City) for bounded searches.
- Calculate peel depth in meters for per-layer geometric offset.
- For each hierarchical layer, launch simultaneous Photon + Nominatim queries.
- Use
Promise.all()to minimize RTT per layer (40% latency reduction vs. sequential). - Implement global API delay post-batch to respect provider rate limits.
For each search result batch:
-
Exact Match (
confidence: "high"):- Detect house number or alley token in result metadata.
- Apply peel-depth offset along geometry path if available.
- Return immediately with
markerTone: "default".
-
Geometric Interpolation (
confidence: 0.8,isInterpolated: true):- Find numeric neighbors (±2, ±4, ... ±30 increment).
- Compute linear interpolation between anchors.
- Use curved path geometry if available (GeoMath.followPathDistance).
- Return virtual house node with
markerTone: "orange".
-
Alley Offset (
confidence: 0.6, Vietnamese-specific):- Detect "Alley XYZ" token in query and results.
- Apply 20-meter perpendicular offset from street centerline.
-
Fuzzy Best-Guess (
confidence: 0.5,isFuzzy: true):- Score all accumulated results across all layers.
- Apply -250 penalty for territory mismatch (safety valve).
- Return highest-scoring result with
markerTone: "GREY".
- Store finalized result in LRU cache with stable key.
- Key format:
${raw}|${lat.toFixed(4)}|${lng.toFixed(4)}.
Why:
- Minimize redundant API calls for repeat queries.
- Reduce external API costs and latency.
How:
- 50-entry limit with automatic LRU eviction.
- Separate cache for finalized location results and Photon query anchors.
- Cache key stability: normalized query + center coordinates (4 decimals).
Example:
// First call: network I/O (500ms)
const result = await resolveLocation("56/1 Flower Street, Lake Ward", mapContext);
// Identical follow-up call: cache hit (< 50ms)
const same = await resolveLocation("56/1 Flower Street, Lake Ward", mapContext);Why:
- Keep core logic provider-agnostic and testable.
- Enable swappable mock API clients for unit testing.
How:
- Services accept injected dependencies rather than importing directly.
- Main coordinator (
locationService.js) wires up API clients at runtime. - Interpolator accepts
depsobject:{ searchPhoton, searchNominatim, extractStreetFragment, ... }.
Example:
// Coordinator injects dependencies:
const result = await Interpolator.buildInterpolationCandidate(
query,
targetHouse,
district,
mapContext,
seedResults,
{
searchPhoton, // Injected from geoRepository
searchNominatim, // Injected from geoRepository
extractStreetFragment, // Injected from addressUtils
inferStreetFromResults, // Injected from addressUtils
}
);Provider Switching:
- Photon → Nominatim: If Photon fails, automatically retry with Nominatim.
- OSRM Primary ↔ Fallback: If primary OSRM endpoint times out, switch to secondary.
Rate-Limit Handling:
- Detect HTTP 429 response.
- Parse
Retry-Afterheader (seconds or HTTP date). - Automatically backoff and retry within timeout window.
- Return normalized error to application layer for UI feedback.
Timeout Enforcement:
- 12-second timeout per request (configurable).
- AbortController-based cancellation.
- Prevents hanging requests from stalling the UI.
API Delay Pacing:
- 400ms delay between batch geocoding requests.
- Prevents provider rate-limit triggers during hierarchical searches.
All resolution methods return a standardized LocationObject for UI rendering and business logic:
{
lat: number, // Decimal latitude (-90 to 90)
lng: number, // Decimal longitude (-180 to 180)
label: string, // Human-friendly display label
// Confidence & semantics
confidence: number | string, // 1.0 (manual), "high" (exact), 0.8 (interpolated), 0.7 (single anchor), 0.6 (alley), 0.5 (fuzzy)
confidenceLabel: string, // "MANUAL_COORDS", "EXACT", "INTERPOLATED", "FUZZY_GUESS", etc.
matchType: string, // "exact", "interpolated", "approximate", "best-guess"
// UI metadata
markerTone: string, // "default" (high confidence), "orange" (computed), "grey" (low confidence), "pink" (favorite)
provider: string, // "manual", "PHOTON", "NOMINATIM", "GEOMATH_INTERPOLATED", etc.
// Transparency flags
isInterpolated: boolean, // True if virtual node (computed between anchors)
isFuzzy: boolean, // True if low-confidence best-guess
isImprecise: boolean, // True if geometric approximation
needsVerification: boolean, // True if user should verify before dispatch
// Optional fields
method: string, // "PHOTON (EXACT)", "GEOMATH (APPROXIMATE)", etc.
sourceType: string, // Result POI type from provider
snappedToPoi: boolean, // True if snapped to nearby POI
districtMatch: boolean, // True if matches expected district anchor
}UI Rendering Examples:
// High-confidence result (green marker)
if (location.confidence === "high") mapMarker.setStyle({ color: "green" });
// Interpolated result (orange marker with caution badge)
if (location.isInterpolated) {
mapMarker.setStyle({ color: "orange" });
showBadge("COMPUTED", "This location was interpolated.");
}
// Fuzzy result (grey marker, requires verification)
if (location.isFuzzy) {
mapMarker.setStyle({ color: "grey" });
showBadge("LOW CONFIDENCE", "Verify before dispatch.");
}A core design principle enables incremental provider migration without rewriting application logic:
- API abstraction: The application layer calls
searchPhoton()andsearchNominatim(), not hardcoded provider details. - Error normalization: All provider errors (timeouts, 429, CORS) are normalized to standard exception types.
- Request policy: Timeout, retry count, backoff strategy defined once in the data layer.
- Response mapping: Provider-specific response formats are normalized to
LocationObjectin the data layer.
Migration path example:
- Replace
https://router.project-osrm.org→ your private OSRM instance. - Update only
geoRepository.jsendpoint URLs. - No changes needed in
locationService.js,uiManager.js, ormapController.js.
The route interaction subsystem uses a Ghost Path technique for high-precision UX:
- Visible route polyline: user-facing blue route.
- Interaction polyline: thick, invisible path on top (
weight: 20,opacity: 0) to enlarge the hover/drag hit-box.
Benefits:
- Reliable cursor capture on thin route geometry.
- Stable hover tooltip updates with snapped split-distance math.
- Practical desktop usability for dense map segments.
Waypoint lifecycle is explicitly managed as stateful application logic:
- User inserts a waypoint via route interaction.
- Waypoint gets stable internal ID and marker binding.
- Drag operations update temporary preview geometry.
- Drag end triggers formal OSRM recalculation with full point sequence.
- Context menu / double-click removes waypoint by ID and immediately recalculates route.
- Marker object is removed from the map to avoid ghost artifacts.
This keeps route geometry, UI markers, and internal waypoint arrays consistent across all transitions.
Favorites are treated as first-class reusable coordinates:
- User clicks the heart action on a result card.
- A naming modal captures a human-friendly label.
- The app saves
{ name, lat, lon }tolocalStoragethroughFavRepository. - Favorite dropdown (next to Add Destination) renders all saved entries.
- Selecting a favorite creates a destination row prefilled with coordinates and renders a light-pink marker (
marker-pink) for quick visual differentiation.
Branch Routes applies multiple anti-flood protections:
- Input autocomplete debounce for origin/destination typing.
- Provider pacing delays in geocoding fallback loops.
- Throttled route hover computations for high-frequency mouse events.
Together these controls reduce API pressure and avoid jitter under rapid UI input.
Loading feedback is reference-counted and explicit:
showLoading()increments active loading operations.hideLoading()decrements or force-resets state.- Overlay visibility is driven by operation state, preventing spinner leaks.
This is especially important around async route recalculation and recoverable provider failures.
Branch Routes uses a practical, logistics-oriented UI language:
- Font Awesome 6 floating control stack for quick global actions.
- Structured sidebar with sticky controls for repeated operational use.
- Distance chips, method badges, and verification cues for fast decision-making.
- Hover route tooltip for split-distance context (
from start/to destination). - Favorite control stack with heart actions, naming modal, and saved-location dropdown for repeat operations.
- Modern browser with ES module support.
- Network access to external map/geocoding/routing providers.
Because the project uses ES modules (type="module"), run via a local HTTP server (not file://).
Option 1: VS Code Live Server
- Open the project folder.
- Start Live Server.
- Open the generated localhost URL.
Option 2: Python simple server
cd "path to the project"
python -m http.server 8080Then open:
http://localhost:8080
Option 3: Vite
- Init and install (if not yet)
npm install- Run
npm run dev- Photon
https://photon.komoot.io/apihttps://photon.komoot.de/api
- Nominatim
https://nominatim.openstreetmap.org/searchhttps://nominatim.osm.ch/search
- OSRM (primary and fallback)
https://router.project-osrm.org/route/v1/drivinghttps://routing.openstreetmap.de/routed-car/route/v1/driving
Repository-level behavior includes:
- Multi-endpoint fallback.
- Timeout handling.
- Retry with backoff.
- Rate-limit awareness (
429/Retry-After). - Error normalization for application-level handling.
index.html
style.css
js/
main.js
controllers/
mapController.js
repositories/
favRepository.js
geoRepository.js
services/
geoMath.js
locationService.js
routeInteractionService.js
ui/
uiManager.js
modalFactory.js
overlayManager.js
rowManager.js
debounceUtil.js
This project intentionally favors maintainable backend-style patterns in frontend code:
- Explicit orchestration (
main.js). - Modular service contracts.
- Deterministic state transitions for map interaction.
- Failure-aware async boundaries.
That approach keeps Branch Routes scalable as provider logic, geospatial math, and route interaction complexity continue to grow.
Ehe update later.