Best Practices

AI Rules for Geolocation Features

AI requests location permission on page load with no explanation. Rules for progressive permission requests, IP geolocation fallback, PostGIS spatial queries, privacy-preserving location, and geocoding best practices.

7 min read·March 16, 2025

Location permission prompt on page load — 60% deny, feature is dead, no fallback

Progressive permissions, IP fallback, PostGIS spatial queries, privacy-preserving coordinates, reverse geocoding

AI Asks for Location Before Saying Hello

AI generates geolocation with: an immediate permission prompt on page load (user has no context for why their location is needed — they deny), no fallback when denied (feature completely broken without GPS permission), storing exact coordinates (latitude/longitude to 6 decimal places — pinpoints a user to within 1 meter), no spatial indexing (distance calculations in application code instead of database — O(n) per query), and no geocoding (raw coordinates displayed instead of readable addresses). The user denies the permission prompt and the feature is dead.

Modern geolocation is: progressively requested (ask for location when the user initiates a location-dependent action, not on page load), fallback-equipped (IP geolocation for approximate location when GPS is denied), privacy-preserving (round coordinates to neighborhood precision, not meter precision), spatially indexed (PostGIS for efficient nearby queries), and geocoded (coordinates converted to readable addresses via reverse geocoding). AI generates none of these.

These rules cover: progressive permission UX, IP geolocation fallback, PostGIS spatial queries, privacy-preserving coordinate handling, reverse geocoding, and timezone detection.

Rule 1: Progressive Permission Requests

The rule: 'Never request location permission on page load. Request it when the user performs an action that clearly needs location: clicking "Find stores near me," tapping the "Use my location" button on a search form, or opening a map view. Before the browser permission prompt, show a custom in-app explanation: "We need your location to show nearby stores. Your location is not stored or shared." Then trigger navigator.geolocation.getCurrentPosition(). The user understands why, grants permission, and the feature works.'

For the permission flow: '(1) User clicks "Find nearby stores." (2) App checks if permission was previously granted: const status = await navigator.permissions.query({ name: "geolocation" }). (3) If granted: get location silently. (4) If prompt: show custom explanation, then request. (5) If denied: show IP-based fallback results with a note: "Showing approximate results. Enable location for precise results." Every state has a path forward — no dead ends.'

AI generates: useEffect(() => { navigator.geolocation.getCurrentPosition(setLocation); }, []) — permission prompt on mount, before the user has any context. 60% of users deny location requests they do not understand (Google research). With progressive permission: the user initiated the action, they understand the context, grant rate exceeds 80%. Same API call, dramatically different grant rate.

  • Request on user action, not page load: 'Find nearby' button, map view, search form
  • Custom explanation before browser prompt: why, what is stored, how to revoke
  • Check permission status first: granted (silent), prompt (explain then ask), denied (fallback)
  • Every state has a path forward: GPS results, IP fallback, or manual location entry
  • 60% deny without context vs 80%+ grant with context — timing is everything
💡 60% Deny vs 80% Grant

Location prompt on page load: 60% deny (no context). Progressive permission after 'Find nearby stores' click with custom explanation: 80%+ grant. Same browser API, same permission dialog. The difference is timing and context.

Rule 2: IP Geolocation Fallback

The rule: 'When GPS permission is denied or unavailable, use IP geolocation for approximate location. IP geolocation: accurate to city level (10-50km radius), no permission required, works on all devices. Services: MaxMind GeoIP2, ipinfo.io, Cloudflare CF-IPCountry header (free, country only). Use for: default city in a search, approximate timezone detection, country-specific content, and regional pricing. Do not use for: turn-by-turn navigation, meter-precision features, or any use case requiring exact position.'

For the Cloudflare approach: 'Cloudflare provides geolocation headers on every request (no API call needed): CF-IPCountry (country code), CF-IPCity (city name), CF-IPLatitude/CF-IPLongitude (approximate coordinates), CF-Timezone (IANA timezone). In Next.js middleware or route handlers: const country = req.headers.get("cf-ipcountry"); const city = req.headers.get("cf-ipcity"). Zero latency (headers are pre-populated), zero cost (included in Cloudflare plan), and no third-party API dependency.'

AI generates: no fallback when GPS is denied. The "nearby stores" feature shows an empty page with "Please enable location services." The user does not want to enable GPS for a casual search. With IP fallback: the page shows stores near their city (IP-detected). Less precise, but functional. The user can refine by entering a zip code or address. Something is always better than nothing.

Rule 3: PostGIS Spatial Queries for Nearby Search

The rule: 'Use PostGIS for spatial queries instead of calculating distances in application code. Add a geography column: ALTER TABLE stores ADD COLUMN location geography(Point, 4326). Create a spatial index: CREATE INDEX idx_stores_location ON stores USING GIST(location). Nearby query: SELECT * FROM stores WHERE ST_DWithin(location, ST_MakePoint(lng, lat)::geography, 10000) ORDER BY ST_Distance(location, ST_MakePoint(lng, lat)::geography). This finds stores within 10km, sorted by distance, using the spatial index — O(log n), not O(n).'

For why not application-level distance: 'Application-level: SELECT * FROM stores (load all 10,000 stores), then calculate distance in JavaScript for each, then filter, then sort. Database load: 10,000 rows transferred. CPU: 10,000 Haversine formula calculations. With PostGIS: the spatial index eliminates 99% of rows before they leave the database. Only nearby stores are returned, already sorted. 10,000 rows transferred vs 20 rows transferred. 500x less data, 500x faster.'

AI generates: const stores = await db.select().from(storesTable); const nearby = stores.filter(s => haversine(userLat, userLng, s.lat, s.lng) < 10000).sort((a, b) => haversine(userLat, userLng, a.lat, a.lng) - haversine(userLat, userLng, b.lat, b.lng)); — all stores loaded, filtered in JS, sorted in JS. PostGIS: one SQL query, spatial index, 20 results returned directly. Same result, the database does the spatial math.

  • geography(Point, 4326) column with GIST spatial index
  • ST_DWithin for radius search: find within N meters, uses spatial index
  • ST_Distance for sorting: nearest first, calculated in the database
  • O(log n) with spatial index vs O(n) with application-level filtering
  • Neon Postgres supports PostGIS: CREATE EXTENSION postgis
⚠️ 10,000 Rows vs 20 Rows

Application-level: load all 10,000 stores, calculate Haversine for each, filter, sort in JavaScript. PostGIS: one SQL query with spatial index, 20 nearby results returned directly. 500x less data transferred, 500x faster. The database does the spatial math.

Rule 4: Privacy-Preserving Location Handling

The rule: 'Round coordinates to preserve privacy. GPS gives 6 decimal places (1.1m precision) — this pinpoints a user to their desk. For most features, neighborhood precision (2-3 decimal places, 100m-1km) is sufficient. Round on receipt: const lat = Math.round(rawLat * 100) / 100 (2 decimal places = 1.1km). Store the rounded value, never the raw coordinates. For analytics and heatmaps: aggregate to grid cells (geohash prefix), never store or display individual user locations.'

For data handling rules: 'Never log exact user coordinates (they end up in log aggregators accessible to the entire team). Never store location history without explicit consent and a clear retention policy. Provide a location data deletion endpoint (GDPR right to erasure). Display location as a neighborhood or city name, never as coordinates ("Downtown Seattle" not "47.6062, -122.3321"). The user shared their location for a feature — not for permanent surveillance.'

AI generates: stores raw GPS coordinates (47.606209, -122.332069) in the user profile, logs them on every request, and displays them in the admin dashboard. Six decimal places: someone looking at the database can find the user exact apartment. Rounded to 2 decimal places (47.61, -122.33): neighborhood-level, sufficient for nearby search, and privacy-preserving. Precision beyond what the feature needs is a liability, not an asset.

ℹ️ 6 Decimal Places = Your Desk

GPS gives 47.606209 (1.1m precision) — pinpoints the user to their desk. Round to 47.61 (2 decimal places, 1.1km): neighborhood-level, sufficient for nearby search, privacy-preserving. Precision beyond what the feature needs is a liability, not an asset.

Rule 5: Reverse Geocoding and Timezone Detection

The rule: 'Convert coordinates to human-readable addresses using reverse geocoding. Services: Google Geocoding API, Mapbox Geocoding, OpenCage, or Nominatim (free, OpenStreetMap-based). Cache results aggressively: the same coordinates (rounded to 3 decimal places) always produce the same address. Display: "123 Main Street, Seattle, WA" or "Downtown Seattle" instead of "47.606, -122.332". The address is what users understand; coordinates are what databases understand.'

For timezone detection: 'Detect the user timezone from: (1) the browser (Intl.DateTimeFormat().resolvedOptions().timeZone — most reliable, no permission needed), (2) IP geolocation (CF-Timezone header or API response), (3) GPS coordinates + timezone lookup (geo-tz library maps coordinates to IANA timezone). Use for: displaying times in the user local timezone, scheduling events in the correct timezone, and showing "Store opens at 9 AM your time" instead of "9 AM PST."'

AI generates: coordinates displayed directly in the UI: "Your location: 47.606209, -122.332069." The user does not know what those numbers mean. Or: times displayed in the server timezone ("9 AM PST") for a user in Tokyo. Reverse geocoding: "Downtown Seattle" is meaningful. Timezone detection: "Opens at 2 AM your time" is actionable. Raw coordinates and server timezones are developer output, not user output.

Complete Geolocation Rules Template

Consolidated rules for geolocation features.

  • Progressive permission: request on user action, custom explanation, every state has a path
  • IP fallback: city-level precision, zero permission needed, Cloudflare headers for free
  • PostGIS spatial queries: GIST index, ST_DWithin for radius, ST_Distance for sorting
  • Privacy: round to 2-3 decimal places on receipt, never store raw 6-decimal GPS
  • Never log exact coordinates — neighborhood precision for analytics, grid cells for heatmaps
  • Reverse geocoding: coordinates to addresses, cache aggressively, display human-readable
  • Timezone: Intl API (browser), CF-Timezone (IP), geo-tz (coordinates) — display local times
  • Location data deletion endpoint for GDPR — user shared for a feature, not for surveillance