Geocode delivery addresses when they are saved
Python · Django · beginner · modification
Dispatch keeps asking for coordinates, so addresses are now geocoded as they are saved. The lookup runs in the form's `clean()`, so both checkout and the account page get it (it leaves out `line2`, since flat and unit numbers tend to confuse the geocoder), and the key goes in the `Authorization` header rather than the URL so it stays out of URL-based access logs. Geocoder failures (HTTP errors, bad JSON, a body without a `results` list) are caught: the address is saved with `needs_geocode` set for the nightly job, and the log line names only the exception type. Tested against the provider's sandbox, including an address it cannot find.
Django 5.1 shop, `requests` 2.32 already in `requirements.txt`. It runs under gunicorn with 4 sync workers per container and gunicorn's default 30-second worker timeout, after which gunicorn kills and restarts the worker and the customer gets a 502. `DeliveryAddress` (existing, `delivery/models.py`) has `line1`, `line2` (optional), `city`, `postcode`, `country` (all strings), `lat`/`lng` (nullable floats) and `needs_geocode` (default `True`). `settings.GEOCODER_URL` and `GEOCODER_KEY` are set in every environment. The geocoding provider answers `200` with `{"results": [{"lat": <number>, "lng": <number>}, ...]}`, best match first and an empty list when nothing matches; `lat` and `lng` are always JSON numbers within the valid coordinate range. It usually answers in about 200 ms, but its status page lists incidents where its servers accepted connections and then stopped responding for tens of minutes.
Requirements
- When a customer saves a delivery address (`DeliveryAddressForm`, used by checkout and the account page), look it up with the geocoding API and store its `lat`/`lng`, so dispatch can plan routes. If the geocoder returns no match, the form shows "We couldn't find this address. Please check it."
- Apart from the no-match case above, the geocoder must never stop a customer from saving an address. If it errors, returns something unexpected, or goes 3 seconds without responding (to the connection attempt or while we wait for data), save the address without coordinates (`lat`/`lng` cleared, `needs_geocode = True`) and log a warning; a nightly job geocodes those later.
- Do not log the API key or the customer's address.
Files touched
- delivery/forms.py
--- delivery/forms.py
+import logging
+
+import requests
from django import forms
+from django.conf import settings
from .models import DeliveryAddress
+
+log = logging.getLogger(__name__)
fields = ["line1", "line2", "city", "postcode", "country"]
+ def clean(self):
+ cleaned = super().clean()
+ if self.errors:
+ return cleaned
+
+ query = ", ".join(cleaned[f] for f in ("line1", "city", "postcode", "country"))
+ try:
+ resp = requests.get(
+ settings.GEOCODER_URL,
+ params={"q": query, "limit": 1},
+ headers={"Authorization": f"Bearer {settings.GEOCODER_KEY}"},
+ )
+ resp.raise_for_status()
+ results = resp.json()["results"]
+ location = None if results == [] else (float(results[0]["lat"]), float(results[0]["lng"]))
+ except (requests.RequestException, ValueError, KeyError, IndexError, TypeError, OverflowError) as exc: