Geocoding Service
4
API Endpoints
0
Service Deps
1
Infrastructure
1
DB Schemas
API Endpoints
/healthFlat health check endpoint for infrastructure compatibility.
/search?q={query}Search the shared PostgreSQL cache. On miss, calls Nominatim through the service-level throttle and
/cacheManually cache geocoding results, used by the frontend direct fallback as a non-blocking shared-cache
/statsReturns cache statistics and top active queries.
/cleanupDeletes expired cache entries.
Infrastructure
Full Documentation
Geocoding Cache Service Context
Quick Start:
cd services/geocoding-service && npm run devPort: 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-serviceis retained as Karmyq's shared geocoding cache and external API policy boundary./search,/cache,/stats, and/cleanupuse ADR-074-style{ success, data, message, error }envelopes;/healthkeeps 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.tsas 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
/searcherror 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