Skip to content

Add Geoapify persistent geocoding and offline-capable routing #502

Description

@stef-k

Outcome and authority

This is the single implementation contract for #502. Add Geoapify persistent reverse geocoding, storage-authorized
routing, explicit bounded Location backfill, and the smallest authenticated provider-neutral route/capability API
required by WayfarerMobile #253.

Reuse merged #499 and
#501. Do not duplicate credential, selection, usage, Mapbox consent,
persistent-enrichment, Trip Editor proposal, or routing transport authorities.
#500 consumes this provider-neutral routing boundary.
#505 owns release, deployment, family migration, and go/no-go. No
provider is selected automatically. Do not publicly deploy #502 alone.

Official policy verdict

Official sources were retrieved 2026-08-23. Geoapify expressly permits caching, storage and redistribution of API
results; stored geocoding; Free-plan commercial production use with attribution; and third-party stacks. Routing returns
geometry, distance, time, legs, steps and turn instructions. Current Terms contain no higher-ranking result-expiry or
deletion-on-key-change/account-closure term.

Wayfarer may persist reverse fields and validated routes, redistribute them to the same authenticated user through
Mobile, and use them offline over OSM/Leaflet/Mapsui. Storage and redistribution are explicit. Same-user synchronization
and offline navigation are necessarily covered downstream uses, although the sources do not name Wayfarer’s exact
workflow. Closure, revocation or switching stops new contacts but does not delete retained results under current terms.

Free currently provides 3,000 credits/day and up to 5 RPS. Daily usage is described as credits accumulated per 24 hours,
but no reset timezone is published. This operational unknown is not a policy blocker. Required visible route/map
attribution is linked Powered by Geoapify to https://www.geoapify.com/ (a follow link on Free web surfaces) and linked
© OpenStreetMap contributors to https://www.openstreetmap.org/copyright, including the ODbL notice. About/Data
licences is supplementary. Reverse provenance links to the same licences in relevant data-licence surfaces.

Current production ownership

  • PersonalLocationProviderProfile: one protected credential, independent authorization/configuration generations and
    verification bindings, revocation, and Mapbox-only Permanent consent.
  • PersonalLocationProviderSelection: independent nullable selections/generations.
  • PersonalProviderCredentialService: protect/read/replace/revoke; one Geoapify key may serve both explicitly
    authorized capabilities.
  • PersonalProviderContactGate: selection, credential, authorization, verification, product/cost admission, and
    immutable authority revalidation.
  • GeoapifyUsageGuard/Admission: one shared rolling pool, default 2,500, PostgreSQL lock/database clock/cleanup.
  • Provider settings controller/model/view: masked profile, independent capability states/selections, guard, usage,
    replacement/revocation.
  • ReverseGeocodingService: shared persistent boundary for eight durable flows: API check-in, API logged location, API
    coordinate update, User Location create/update, import missing address, Trip Editor Place create/update.
  • Location/Place: nullable provider/storage-mode/time provenance and failure preservation. Mapbox remains
    Permanent-only with separate consent, verification, meters and provenance.
  • Administrator routing configuration/mappings/UI/services: approved templates, endpoint/attribution/disclosure, limits,
    verification and activation.
  • UserRoutingConfiguration: older OSRM-template authority; it must not become a second Geoapify secret store.
  • Routing resolver/coordinator/budget/pacer/bounded executor/geometry validator/OSRM client: existing gate, concurrency,
    minute budget, pacing, timeout/response bounds, safe transport, validation and cancellation.
  • Proposal generator/protected context/acceptance: transient proposal and explicit concurrency-bound acceptance; only
    acceptance writes Segment geometry/distance/duration.
  • MobileApiController/MobileCurrentUserAccessor: mobile authentication. Current Trip DTO exposes saved Segment
    route/distance/duration/waypoints, but no on-demand route/capability API or instruction-bearing neutral result.

Extend these seams. Do not alter Mapbox or OSRM behavior, server-default routing, existing DTO members, proposal
concurrency/acceptance, or public/mobile fallback.

Cohesive stages and gates

Keep one issue because all responsibilities share one profile/key, credit pool, result rights, attribution and release
boundary. Strict order:

  1. Credential validation, two capability verifications, safe client, conservative cost estimator.
  2. Reverse adapter behind shared boundary; eight-flow preservation, provenance and settings.
  3. Explicit bounded Location backfill.
  4. Routing adapter and normalized instruction-bearing result.
  5. Trip Editor integration and accepted-route provenance.
  6. Authenticated neutral mobile capability/route endpoint.
  7. Attribution, docs, additive migration and proportional evidence.

Stop after any failing gate. Do not leave unused abstractions/UI. No universal SDK, billing/cache/offline framework, or
general job system.

Credential and capability contract

Use one protected geoapify profile/key. Accept trimmed nonblank 1–2048 characters and reject controls/internal
whitespace; syntax never proves validity. Geocoding and routing authorization, generation, verification and selection
remain independent. Verification never selects. Switching retains inactive credential, current history, usage and data.

Replacement increments credential generation and invalidates both verifications while preserving authorizations,
inactive selection intent, usage and data. Revocation removes ciphertext, increments generation, disables both
authorizations, clears verification, prevents contact and preserves usage/results. No secret, ciphertext, authenticated
URL, fingerprint or fragment reaches browser/mobile/log/audit/proposal/troubleshooting.

Do not add Geoapify legal/billing consent. Explicit authorization plus shared-credit, Free-plan, attribution,
coordinate-disclosure, external-use and disabled-guard acknowledgement suffices. Geoapify cannot satisfy or modify
Mapbox consent.

Verification

Both actions are explicit, fake-HTTP-tested, require readable current credential and authorization, need no active
selection/prior verification, and discard response data.

Geocoding makes one admitted-credit GET to https://api.geoapify.com/v1/geocode/reverse with fixed non-personal
lat=0&lon=0&format=geojson&lang=en&limit=1 and internal apiKey. A valid FeatureCollection with a features array
verifies even empty.

Routing makes one admitted-credit two-waypoint walk request with fixed non-personal coordinates within the published
100 km limit, format=json&lang=en&details=instruction_details&type=balanced&traffic=free_flow, and no optional
feature. Require exactly one result, finite nonnegative distance/time, one leg, at least two valid ordered points
preserving anchors, and valid steps/instructions; discard it.

Bind user/provider/credential generation/capability generation/purpose. Revalidate before HTTP and atomically before
status write. Stale authority cannot verify. Pre-admission cancellation costs zero; post-admission counts and cannot
verify. Valid 2xx is Verified; 401/403 Failed; 429/network/timeout/5xx/malformed/oversized Unavailable. Each retry is
newly admitted. Log no secret, coordinate, URL, response or exception URI.

Shared-credit contract

Retain one Geoapify pool. Default enabled limit is 2,500 credits in every rolling 24 hours against 3,000/day context;
label it Wayfarer’s safety window, not provider reset. External usage is unknown.

Before HTTP compute a positive conservative cost, lock guard FOR UPDATE, capture PostgreSQL clock_timestamp(), sum
admissions newer than 24 hours, and insert only if used + cost <= limit. The final credit is allowed; concurrent
contenders cannot both take it. Commit admission before HTTP. Failed contacts and each retry count; stored reuse costs
zero. Invalid cost, pre-contact rejection/cancellation, duplicate/already-enriched work cost zero.

Costs:

  • reverse, geocoding verification and each backfill record: 1;
  • routing baseline: 1 per consecutive waypoint pair; instruction_details has no documented surcharge;
  • alternatives, avoid, route_details, elevation, optimize_stops, transit, approximated transit, matrix/planner and Batch
    API: unsupported;
  • routing verification: 1;
  • production conservative credits per pair: walk 1, bicycle 1, motorcycle 21, drive 21, bus 21. Twenty-one
    covers baseline plus 500 km increments through the published 10,000 km default-vehicle synchronous cap. This over-admits
    because result-distance cost is unknowable pre-contact.

Existing ledger schema supports multi-credit shared rows, locking and index; change only for a proven defect. Disabled
guard still records rolling usage and warns of paid/suspension risk. Lowering below use pauses until age-out; raising
applies next admission; re-enabling never resets. Cleanup only under lock. No catch-up burst.

Reverse-geocoding contract

Add a cohesive GeoapifyReverseGeocodingAdapter consumed by ReverseGeocodingService; callers never choose provider or
receive credentials. Minimal two-adapter dispatch is enough.

Request exactly GET https://api.geoapify.com/v1/geocode/reverse?lat={latitude}&lon={longitude}&format=geojson&lang=en&limit=1&apiKey={key}.
Validate finite/ranged coordinates, invariant formatting and internal-only key.

Require FeatureCollection/features. Business success uses first Feature with object properties and nonblank formatted
or address_line1; empty is no-result and never persists. Map FullAddress=formatted; Address=address_line1, else joined
housenumber/street, else formatted; AddressNumber=housenumber; StreetName=street; PostCode=postcode; Place=first
city/town/village/municipality/county; Region=first state/state_district/county; Country=country.

Trim and bound values; never partial/empty overwrite. Store provider geoapify, storage mode persistent, UTC success
time using #501 fields. Derive attribution from provider identity, not raw HTML.

Use a dedicated typed client with framework logging removed, fixed endpoint, 15-second timeout (30-second hard maximum),
256 KiB cap, cancellation and no internal retry. Map 401/403 authorization, 429 rate-limit, 5xx/network/timeout
unavailable, malformed/oversized/no-result bounded failure. Revalidate before contact and persistence. No
Mapbox/community fallback or sensitive logging.

Explicit bounded backfill

Add one explicit authenticated user action in the Location import/settings domain, not startup, migration or queue. Use
a synchronous cancellable service operation; infer resume from rows and return bounded status. Add no SSE/job framework
unless a real request-lifetime owner proves necessary.

Each invocation handles at most 100 owned Locations ordered by Timestamp then Id where all eight enrichment fields and
provenance are blank/null. Recheck ownership/emptiness immediately before admission and persistence. Any
manual/imported/existing value or provenance excludes the row.

Require selected, authorized, verified Geoapify geocoding. Per row validate, admit 1, contact once, revalidate
authority/predicate and atomically persist. Stop immediately on exhaustion, cancellation, authorization failure, stale
authority or DB failure. Continue past bounded no-result/malformed/temporary failure only within this invocation and
report categories; no automatic retry. Later invocation resumes because successes no longer match. No pending column,
history rewrite, Mapbox cleanup or Batch API.

Expose scanned/succeeded/no-result/unavailable/remaining estimate/exhausted/cancelled without content. At 2,500 credits,
10,000 records require four full rolling allocations and 100,000 forty—roughly four/forty quota-days, longer because
live work shares the pool. No completion promise.

Routing contract

Add Geoapify = 2 to bounded RoutingAdapterType and a Geoapify adapter/client selected by existing orchestration.
Administrators create an enabled Geoapify template with fixed endpoint, attribution/disclosure, limits and mappings;
they never enter/access a key. The user’s selected authorized verified personal profile supplies it. Both current admin
template/mapping and personal authority are required.

Exact initial mappings: walk->walk, bicycle->bicycle, bike->motorcycle, car->drive, bus->bus. Other profiles
are unsupported unless explicitly mapped to one of these enum values. OSRM mappings remain unchanged.

Request GET https://api.geoapify.com/v1/routing with 2–25 finite/ranged lat,lon waypoints, mapped mode,
format=json&lang=en&details=instruction_details&type=balanced&traffic=free_flow, internal key. Segment intermediates
use intermediate_waypoint_mode=stopover; mobile allows at most 5 total points (origin, destination, 3 anchors).
Exclude alternatives and cost-bearing options.

Normalize exactly one complete result to ordered lon/lat geometry, finite nonnegative metres/seconds, ordered legs and
instructions with text/type, from/to indices, distance/duration. Require one leg per pair, all anchors within existing
tolerance, at least two points, continuity, endpoint preservation, valid instructions and consistent totals. Reject
partial route.

Bounds: 25 Trip Editor/5 mobile inputs; 10,000 normalized points before existing persistence budget; 5,000 instructions;
1 MiB default response tunable only within existing 256 KiB–2 MiB; 30-second request hard limit and existing five-minute
proposal limit; duration at most 365 days. Reuse existing concurrency, minute budget, pacing, cancellation, transport,
validation and revalidation. Estimate/admit before each attempt; retry separately admitted. Bounded statuses only. No
public OSRM fallback or sensitive logging.

Existing Trip Editor proposal/acceptance consumes normalized geometry/distance/duration unchanged. Extend internal
proposal with instructions/provenance while preserving protected transient context, aggregate concurrency token, anchor
fingerprint, mapping revalidation and explicit acceptance.

Stored-route ownership

Proposal remains transient. Only explicit acceptance writes existing Segment geometry/distance/automatic duration plus
new nullable Geoapify provenance. Failed generation, refresh or acceptance never clears accepted geometry.

Retain on accepted Segment: neutral ordered geometry, distance, duration, normalized instructions, transport profile,
existing anchors/waypoints, generated UTC, safe provider/configuration identity/version, source/attribution and storage
authority persistent. Never persist SDK object, secret/generation, authenticated URL, raw response or proposal
payload.

On-demand mobile routes have no server domain owner, so Wayfarer returns but does not persist them. Mobile #253 owns
bounded local persistence, matching, replacement and eviction. Backend supplies identities, inputs, time, provenance,
attribution and storage authority.

Provider-neutral mobile API

Add a dedicated authenticated mobile controller/service, not the Trip Editor endpoint. Use existing token/current-user
authority and server-side selection. Mobile supplies no provider, endpoint, template or credential.

Capability query for a bounded profile returns available, no-provider-selected, unauthorized,
verification-required, unsupported-profile, exhausted, rate-limited, temporarily-unavailable or
configuration-changed, plus safe identity/profile/attribution/retention authority when usable. It makes no provider
contact/cost.

Route request accepts finite/ranged origin/destination, at most 3 ordered anchors, and active Wayfarer profile identity.
Reject unknown fields, provider URLs/choices, duplicates, excess or oversize before admission. Authenticate first and
apply existing user generation budget.

Success returns bounded category, neutral geometry, metres/seconds, normalized instructions, profile, exact match
points, generated UTC, stable server identity, safe provider/template identity/version, source, both attribution
entries, and persistent authority with no current provider expiry. Errors are bounded.

Create an additive endpoint/DTO; existing mobile ignores optional additions. New mobile treats missing/404/disabled
capability as unavailable and uses saved Segment geometry or Direct. No immediate mobile upgrade.

Offline retention and attribution

Retain Geoapify results indefinitely under current rights; quota cycle is not expiry. Enrichment and accepted routes
survive inactive credentials, replacement, switching, outage, plan change and closure. These only stop or alter new
contacts. Failed refresh preserves previous value; replacement is atomic; offline never deletes.

Every route carries geoapify, safe configuration identity, generated time, persistent and both attributions.
Whenever displayed or used—Trip Editor, accepted Segment, mobile online/offline—show linked Powered by Geoapify and © OpenStreetMap contributors; About/Data licences includes the ODbL link. Reverse provenance links to licences in
relevant non-map surfaces.

No reuse across server, provider, configuration, profile, endpoints or anchors. Mobile #253 owns exact local matching,
retention, age and fallback. Backend owns stable identities, validated payload, rights and attribution.

Security and logging

Query-auth apiKey requires dedicated typed clients with RemoveAllLoggers() or equivalent, fixed hosts, and bounded
exception translation inside adapters. Raw HttpRequestException, RequestMessage or RequestUri never crosses.

Never log/audit key, ciphertext, full URL/query, coordinates, addresses, notes, geometry, instructions, raw
payload/body. Never expose through verification, DTO, browser/mobile or troubleshooting. Allowed: bounded
user/provider/product/intent/category, cost/limit/use, safe configuration/generation ids and coarse status. No secret
fingerprint.

Disclose Geoapify’s policy: request body, headers, IP and timestamp are stored for access/usage/statistics;
successful-request data generally no longer than 24 hours. State that coordinates/routes/addresses go
server-to-provider/CDNs; do not promise identical retention for failed requests.

Failure and preservation

  • missing/unreadable/revoked credential: credential-required, no contact;
  • unauthorized: no contact; unverified: verification-required, no contact; no selection: no contact;
  • exhausted: no contact and recovery as admissions age; invalid/overflow cost: no contact;
  • 401/403: authorization-failed, counted, current capability verification atomically Failed;
  • 429: rate-limited, counted; timeout/network/5xx: temporarily-unavailable, counted;
  • cancellation propagates; counts only committed admission; no fabricated success;
  • malformed/oversized/invalid coordinate/geometry/instruction: invalid-response, counted;
  • stale before contact: configuration-changed, no contact; after contact: counted/no persistence;
  • database failure remains real failure/rollback; partial backfill reports and resumes; outage never falls back.

Provider failures do not block tracking/sync, erase enrichment/manual Place/accepted Segment/valid mobile route, or
strand imports. Direct remains available. Database failures remain distinct.

User and administrator workflow

One accessible Geoapify card, ordered: masked credential/replacement; geocoding authorization; geocoding verification;
geocoding selection; routing authorization; routing verification; routing selection; one shared guard; total
usage/exhaustion; backfill status/action; revocation.

Show independent states, associated labels, validation feedback, no auto-selection, inactive retention,
replacement/revocation effects, exhaustion recovery and preservation. Explain default 2,500 rolling limit, one shared
pool (never separate allowances), 3,000/day context, unknown external use, dedicated-key recommendation, prohibition on
spreading calls across accounts/projects to evade limits, disabled-guard paid/suspension risk, attribution and privacy.

Admins configure support/enablement, approved mappings, fixed endpoint,
pacing/concurrency/timeout/response/input/instruction limits, attribution/disclosure. They never enter, access or use an
admin Geoapify key. Admin template verification is offline structural validation; personal verification is the provider
contact. Both authorities are required.

Schema and migration boundary

Use one additive EF migration where practical. Existing profile verification/provenance/selection/usage needs no
duplicate fields. Add only:

  • nullable Segment.RouteInstructionsJson with bounded neutral schema/length;
  • nullable Segment.RouteProvider varchar(24);
  • nullable Segment.RouteProviderConfigurationId and configuration version;
  • nullable Segment.RouteGeneratedAt UTC;
  • nullable Segment.RouteStorageMode varchar(16);
  • nullable bounded attribution/source identity only if not deterministically derivable.

Add Geoapify adapter discriminator and exact mapping validation/constraints. Historical Segments remain null. No
deletion/rewrite, secret duplication, raw payload/URL/SDK object, pending backfill or speculative-provider schema.
Preserve table mappings/FKs. Prove apply/rollback boundary, snapshot and zero pending model changes.

Documentation and release coordination

Extend canonical personal-provider, services, importing/exporting, mobile, API, database, security, troubleshooting,
routing/Trip Editor, README/features/architecture and About/Data licences documentation.

Document setup, Free conditions, attribution, shared costs/guard, both verifications, backfill, offline rights,
privacy/log retention, switching/revocation, exhaustion, admin setup, Mobile #253 and Mapbox separation.

Follow #505: #502 merges before #500; backend/mobile may validate locally before deployment; one backend candidate
includes #499/#501/#502/#500; back up PostgreSQL and Data Protection together; backend deploys first; family profiles
after deploy; minimal authorized production smoke tests; mobile publishes only after API/device acceptance. Never
publicly deploy #502 alone.

Proportional evidence

Controlled fake HTTP only; no public/billable automated call:

  • credential hygiene, independent generation binding and both verification/stale races;
  • cost table, invalid costs, shared aggregation/retries/guard transitions/cleanup, PostgreSQL concurrent final credit;
  • reverse parsing/mapping/status/size/cancel/provenance and eight-flow preservation;
  • backfill ownership/filter/order/recheck/100 bound/exhaustion/cancel/partial/resume;
  • routing mapping/exclusions/cost/response/legs/instructions/geometry/anchors/bounds/pacing/timeout/stale authority;
  • one proposal-to-explicit-acceptance integration proving transient proposal, stored provenance/instructions and
    failed-refresh preservation;
  • authenticated mobile capability/route bounds/categories/input privacy/provider-disabled behavior;
  • route attribution/provenance and additive old-mobile JSON compatibility;
  • captured diagnostics exclude secret, URL, coordinates, addresses, geometry and instructions;
  • one stable settings assertion for order, shared allowance, accessibility and attribution;
  • PostgreSQL only for migration, locking, concurrency and durability; no exhaustive browser/device/provider matrix.

No mounted backend/browser workflow is required because stable seams cover behavior. Add at most one only if a behavior
cannot be credibly covered below; infrastructure failure is unavailable evidence, not product failure.

Architecture and LOC

Cohesive owners: reverse adapter/parser; route adapter/parser/cost estimator; existing gate/ledger only as needed;
bounded backfill coordinator; existing proposal integration; dedicated mobile route controller/service/DTO; Segment
migration; settings/docs. Keep provider code out of large controllers and isolate Mapbox/OSRM.

Run LOC check. No new/changed source above 600 counted LOC. At 400 review cohesion and justify or split by real
responsibility; never split only for the number or add unused interfaces.

Exclusions

No default/shared Wayfarer key, direct mobile provider call, Geoapify tiles, forward geocoding, automatic history,
queue/general jobs, Batch API, alternatives/avoid/elevation/route-details/optimization, public OSRM fallback, Mapbox
change/consent substitution, history cleanup, SDK/raw storage, generic billing/cache/offline framework, prefetch or
forced mobile upgrade.

Completion criteria

Complete only when stages 1–7 pass, migration/model is clean, sources/retrieval date remain documented, #505 is aligned
without duplicated authority, no public automated provider call occurred, and branch failures are resolved or honestly
classified.

Official sources — retrieved 2026-08-23

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions