Docs·ba2a6d13·Updated Jul 17, 2026·85 ADRs
All Services

Geocoding Service

Port 3009productionoptional

4

API Endpoints

0

Service Deps

1

Infrastructure

1

DB Schemas

API Endpoints

GET
/health

Flat health check endpoint for infrastructure compatibility.

GET
/search?q={query}

Search the shared PostgreSQL cache. On miss, calls Nominatim through the service-level throttle and

POST
/cache

Manually cache geocoding results, used by the frontend direct fallback as a non-blocking shared-cache

GET
/stats

Returns cache statistics and top active queries.

POST
/cleanup

Deletes expired cache entries.

Infrastructure

postgres

Full Documentation

Geocoding Cache Service Context

Quick Start: cd services/geocoding-service && npm run dev Port: 3009 | Health: http://localhost:3009/health

Purpose

geocoding-service is Karmyq's shared backend geocoding cache and external geocoder policy boundary. It keeps browser autocomplete local-cache-first, shares PostgreSQL cache hits across users, centralizes Nominatim application identification, and throttles outbound public Nominatim calls.

Sprint 109 - Geocoding Cache Hardening (2026-06-22)

  • geocoding-service is retained as Karmyq's shared geocoding cache and external API policy boundary.
  • /search, /cache, /stats, and /cleanup use ADR-074-style { success, data, message, error } envelopes; /health keeps the flat health shape for infrastructure compatibility.
  • Outbound Nominatim calls are centrally throttled and mocked in tests.
  • Frontend remains local-cache-first, backend-cache-second, direct external fallback last.
  • Dependency docs now reflect PostgreSQL, not Redis, and apps/frontend/src/lib/geocoding.ts as the application consumer.

Database Schema

Tables Owned by This Service

CREATE TABLE geocoding_cache (
    query TEXT PRIMARY KEY,
    results JSONB NOT NULL,
    cached_at TIMESTAMP DEFAULT NOW(),
    expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '30 days',
    hit_count INTEGER DEFAULT 1,
    last_accessed TIMESTAMP DEFAULT NOW(),
    source VARCHAR(50) DEFAULT 'nominatim'
);

CREATE INDEX idx_geocoding_expires_at ON geocoding_cache(expires_at);
CREATE INDEX idx_geocoding_hit_count ON geocoding_cache(hit_count DESC);
CREATE INDEX idx_geocoding_last_accessed ON geocoding_cache(last_accessed DESC);

Tables Read by This Service

  • None; the cache table is service-specific and global.

Architecture

The service is Tier 2 in the frontend geocoding flow:

User Request
  -> Tier 1: IndexedDB common locations / API cache
  -> Tier 2: PostgreSQL shared cache (this service)
  -> Tier 3: localStorage legacy cache
  -> Tier 4: direct Nominatim fallback only for backend reachability failures

The backend owns the normal Nominatim path. Direct browser-to-Nominatim calls are last-resort fallback only and must not become the primary autocomplete path.

API Endpoints

GET /health

Flat health check endpoint for infrastructure compatibility.

{
  "status": "healthy",
  "service": "geocoding-cache",
  "port": "3009"
}

GET /search?q={query}

Search the shared PostgreSQL cache. On miss, calls Nominatim through the service-level throttle and caches successful results.

{
  "success": true,
  "data": {
    "results": [
      {
        "display_name": "Oakland, Alameda County, California, United States",
        "address": "Oakland",
        "lat": 37.8044,
        "lng": -122.2712,
        "type": "city"
      }
    ],
    "source": "cache",
    "cached": true
  }
}

Invalid queries return:

{
  "success": false,
  "message": "Query must be at least 2 characters",
  "error": "INVALID_QUERY"
}

POST /cache

Manually cache geocoding results, used by the frontend direct fallback as a non-blocking shared-cache write.

{
  "query": "oakland",
  "results": [
    {
      "display_name": "Oakland, Alameda County, California, United States",
      "address": "Oakland",
      "lat": 37.8044,
      "lng": -122.2712,
      "type": "city"
    }
  ]
}

Response:

{
  "success": true,
  "data": {
    "query": "oakland"
  },
  "message": "Cached results for: oakland"
}

GET /stats

Returns cache statistics and top active queries.

{
  "success": true,
  "data": {
    "stats": {
      "total_entries": "1",
      "total_hits": "3",
      "active_entries": "1",
      "expired_entries": "0",
      "avg_hit_count": 3,
      "max_hit_count": 3
    },
    "top_queries": [
      {
        "query": "oakland",
        "hit_count": 3,
        "last_accessed": "2025-12-27T04:59:00.844Z"
      }
    ]
  }
}

POST /cleanup

Deletes expired cache entries.

{
  "success": true,
  "data": {
    "deleted": 5
  },
  "message": "Deleted 5 expired cache entries"
}

Dependencies

External Services

  • Nominatim API: https://nominatim.openstreetmap.org/search
    • Public policy boundary is centralized in this service.
    • Outbound calls use Karmyq User-Agent.
    • Outbound calls are throttled to at most one call per second per process.

Infrastructure

  • PostgreSQL 15+ for geocoding_cache.

Application Consumers

  • apps/frontend/src/lib/geocoding.ts

Service Dependencies

  • None.

Implementation

  • src/geocodingApp.js: Express app factory, middleware, rate limiters, and route registration.
  • src/geocodingService.js: query normalization, validation, cache reads/writes, Nominatim call mapping, and outbound throttle.
  • src/response.js: ADR-074 response helpers.
  • index.js: server bootstrap and PostgreSQL connection setup.

Testing

npm --workspace=geocoding-service test
npm --workspace=geocoding-service run test:unit
npm --workspace=geocoding-service run test:regression

Current coverage:

  • Unit tests for normalization, validation, outbound throttling, and throttle recovery after rejection.
  • Regression tests for /search error envelopes, cache hit behavior without external fetch, and recovery after transient external geocoder rejection.

External Nominatim calls must be mocked in tests.

Common Tasks

curl http://localhost:3009/health
curl "http://localhost:3009/search?q=Oakland"
curl http://localhost:3009/stats
curl -X POST http://localhost:3009/cleanup

Manual cache entry:

curl -X POST http://localhost:3009/cache \
  -H "Content-Type: application/json" \
  -d '{"query":"oakland","results":[{"display_name":"Oakland, CA","address":"Oakland","lat":37.8044,"lng":-122.2712,"type":"city"}]}'

Environment Variables

PORT=3009
DATABASE_URL=postgres://user:password@host:5432/karmyq
DB_HOST=localhost
DB_PORT=5432
DB_NAME=karmyq
DB_USER=karmyq_user
DB_PASSWORD=karmyq_password_dev
ALLOWED_ORIGINS=http://localhost:3000

Known Issues & Future Enhancements

  • No authentication; the cache is global and must not store user/community-specific state.
  • Cache invalidation is time-based only.
  • Reverse geocoding remains out of scope.
  • Paid provider migration and self-hosted Nominatim remain future decisions.

Status: Production Version: 11.17.0 Last Updated: 2026-06-22