From 4e0cc65ba2e1a95c5894e29341e0315a7a6f381e Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Mon, 2 Mar 2026 01:00:07 -0800 Subject: [PATCH] Auto-geocode addresses on create/update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract geocoding functions into shared api/inc/geocode.cfm and call geocodeAddressById() via cfthread after every address INSERT or UPDATE. Uses OpenStreetMap Nominatim (free, no API key). Non-blocking — the HTTP call runs in a background thread so responses aren't delayed. Affected endpoints: - setup/saveWizard.cfm (new business creation) - businesses/update.cfm (business address update) - portal/updateSettings.cfm (portal settings save) - addresses/add.cfm (customer delivery address) Co-Authored-By: Claude Opus 4.6 --- api/addresses/add.cfm | 8 +++++ api/admin/_scripts/geocode.cfm | 46 +-------------------------- api/businesses/update.cfm | 11 +++++++ api/inc/geocode.cfm | 58 ++++++++++++++++++++++++++++++++++ api/portal/updateSettings.cfm | 9 ++++++ api/setup/saveWizard.cfm | 8 +++++ 6 files changed, 95 insertions(+), 45 deletions(-) create mode 100644 api/inc/geocode.cfm diff --git a/api/addresses/add.cfm b/api/addresses/add.cfm index 09e5ff7..e441ff1 100644 --- a/api/addresses/add.cfm +++ b/api/addresses/add.cfm @@ -116,6 +116,14 @@ try { addedOn: { value: now(), cfsqltype: "cf_sql_timestamp" } }, { datasource: "payfrit" }); + // Auto-geocode address in background + + + + + + + // Get state info for response qState = queryTimed("SELECT Abbreviation as StateAbbreviation, Name as StateName FROM tt_States WHERE ID = :stateId", { stateId: { value: stateId, cfsqltype: "cf_sql_integer" } diff --git a/api/admin/_scripts/geocode.cfm b/api/admin/_scripts/geocode.cfm index 4c88e7a..77aa952 100644 --- a/api/admin/_scripts/geocode.cfm +++ b/api/admin/_scripts/geocode.cfm @@ -26,51 +26,7 @@

Address Geocoding

Auto-geocode addresses using OpenStreetMap Nominatim (free, no API key required)

- -function geocodeAddress(addressString) { - var result = { "success": false, "error": "" }; - if (len(trim(addressString)) EQ 0) { - result.error = "Empty address"; - return result; - } - - try { - var httpService = new http(); - httpService.setMethod("GET"); - httpService.setUrl("https://nominatim.openstreetmap.org/search?q=" & urlEncodedFormat(addressString) & "&format=json&limit=1"); - httpService.addParam(type="header", name="User-Agent", value="Payfrit/1.0"); - httpService.setTimeout(10); - - var httpResult = httpService.send().getPrefix(); - - if (httpResult.statusCode CONTAINS "200") { - var data = deserializeJSON(httpResult.fileContent); - if (arrayLen(data) GT 0) { - result.success = true; - result.lat = data[1].lat; - result.lng = data[1].lon; - return result; - } - result.error = "No results found"; - return result; - } - result.error = "HTTP " & httpResult.statusCode; - return result; - } catch (any e) { - result.error = e.message; - return result; - } -} - -function buildAddressString(line1, line2, city, zipCode) { - var parts = []; - if (len(trim(line1))) arrayAppend(parts, trim(line1)); - if (len(trim(line2))) arrayAppend(parts, trim(line2)); - if (len(trim(city))) arrayAppend(parts, trim(city)); - if (len(trim(zipCode))) arrayAppend(parts, trim(zipCode)); - return arrayToList(parts, ", "); -} - + diff --git a/api/businesses/update.cfm b/api/businesses/update.cfm index ffa4b63..902cca7 100644 --- a/api/businesses/update.cfm +++ b/api/businesses/update.cfm @@ -111,6 +111,7 @@ try { zip: zip, addrID: qAddr.ID }, { datasource: "payfrit" }); + geocodeAddrId = qAddr.ID; } else { // Create new address queryTimed(" @@ -123,8 +124,18 @@ try { zip: zip, bizID: businessId }, { datasource: "payfrit" }); + qNewAddr = queryTimed("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" }); + geocodeAddrId = qNewAddr.id; } + // Auto-geocode address in background +
+ + + + + + response.OK = true; } catch (any e) { diff --git a/api/inc/geocode.cfm b/api/inc/geocode.cfm new file mode 100644 index 0000000..a453ee6 --- /dev/null +++ b/api/inc/geocode.cfm @@ -0,0 +1,58 @@ + +function geocodeAddress(addressString) { + var result = { "success": false, "error": "" }; + if (!len(trim(addressString))) { result.error = "Empty address"; return result; } + try { + var httpService = new http(); + httpService.setMethod("GET"); + httpService.setUrl("https://nominatim.openstreetmap.org/search?q=" & urlEncodedFormat(addressString) & "&format=json&limit=1"); + httpService.addParam(type="header", name="User-Agent", value="Payfrit/1.0"); + httpService.setTimeout(10); + var httpResult = httpService.send().getPrefix(); + if (httpResult.statusCode CONTAINS "200") { + var data = deserializeJSON(httpResult.fileContent); + if (arrayLen(data) GT 0) { + result.success = true; + result.lat = data[1].lat; + result.lng = data[1].lon; + return result; + } + result.error = "No results found"; + } else { + result.error = "HTTP " & httpResult.statusCode; + } + } catch (any e) { result.error = e.message; } + return result; +} + +function buildAddressString(line1, line2, city, zipCode) { + var parts = []; + if (len(trim(line1))) arrayAppend(parts, trim(line1)); + if (len(trim(line2))) arrayAppend(parts, trim(line2)); + if (len(trim(city))) arrayAppend(parts, trim(city)); + if (len(trim(zipCode))) arrayAppend(parts, trim(zipCode)); + return arrayToList(parts, ", "); +} + +function geocodeAddressById(addressId) { + var qAddr = queryExecute(" + SELECT Line1, Line2, City, ZIPCode + FROM Addresses WHERE ID = :id + ", { id: addressId }, { datasource: "payfrit" }); + + if (qAddr.recordCount == 0 || !len(trim(qAddr.Line1))) return; + + var fullAddress = buildAddressString(qAddr.Line1, qAddr.Line2, qAddr.City, qAddr.ZIPCode); + var geo = geocodeAddress(fullAddress); + + if (geo.success) { + queryExecute(" + UPDATE Addresses SET Lat = :lat, Lng = :lng WHERE ID = :id + ", { + lat: { value: geo.lat, cfsqltype: "cf_sql_decimal" }, + lng: { value: geo.lng, cfsqltype: "cf_sql_decimal" }, + id: addressId + }, { datasource: "payfrit" }); + } +} + diff --git a/api/portal/updateSettings.cfm b/api/portal/updateSettings.cfm index a314481..5060698 100644 --- a/api/portal/updateSettings.cfm +++ b/api/portal/updateSettings.cfm @@ -129,6 +129,15 @@ try { if (arrayLen(addrUpdates) > 0) { queryTimed("UPDATE Addresses SET " & arrayToList(addrUpdates, ", ") & " WHERE ID = :addrId", addrParams, { datasource: "payfrit" }); + + // Auto-geocode address in background + geocodeAddrId = qExistingAddr.ID; + + + + + + } } } diff --git a/api/setup/saveWizard.cfm b/api/setup/saveWizard.cfm index 818af9f..f9dc4c7 100644 --- a/api/setup/saveWizard.cfm +++ b/api/setup/saveWizard.cfm @@ -217,6 +217,14 @@ try { addressId = qNewAddr.id; response.steps.append("Created address record (ID: " & addressId & ")"); + // Auto-geocode address in background + + + + + + + // Get community meal type (1=provide meals, 2=food bank donation) communityMealType = structKeyExists(wizardData, "communityMealType") && isSimpleValue(wizardData.communityMealType) ? val(wizardData.communityMealType) : 1; if (communityMealType < 1 || communityMealType > 2) communityMealType = 1;