Auto-geocode addresses on create/update
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 <noreply@anthropic.com>
This commit is contained in:
parent
dc5148d1b8
commit
4e0cc65ba2
6 changed files with 95 additions and 45 deletions
|
|
@ -116,6 +116,14 @@ try {
|
||||||
addedOn: { value: now(), cfsqltype: "cf_sql_timestamp" }
|
addedOn: { value: now(), cfsqltype: "cf_sql_timestamp" }
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Auto-geocode address in background
|
||||||
|
</cfscript>
|
||||||
|
<cfthread action="run" name="geocode_addr_#newAddressId#" addressId="#newAddressId#">
|
||||||
|
<cfinclude template="/api/inc/geocode.cfm">
|
||||||
|
<cfset geocodeAddressById(attributes.addressId)>
|
||||||
|
</cfthread>
|
||||||
|
<cfscript>
|
||||||
|
|
||||||
// Get state info for response
|
// Get state info for response
|
||||||
qState = queryTimed("SELECT Abbreviation as StateAbbreviation, Name as StateName FROM tt_States WHERE ID = :stateId", {
|
qState = queryTimed("SELECT Abbreviation as StateAbbreviation, Name as StateName FROM tt_States WHERE ID = :stateId", {
|
||||||
stateId: { value: stateId, cfsqltype: "cf_sql_integer" }
|
stateId: { value: stateId, cfsqltype: "cf_sql_integer" }
|
||||||
|
|
|
||||||
|
|
@ -26,51 +26,7 @@
|
||||||
<h1>Address Geocoding</h1>
|
<h1>Address Geocoding</h1>
|
||||||
<p>Auto-geocode addresses using OpenStreetMap Nominatim (free, no API key required)</p>
|
<p>Auto-geocode addresses using OpenStreetMap Nominatim (free, no API key required)</p>
|
||||||
|
|
||||||
<cfscript>
|
<cfinclude template="/api/inc/geocode.cfm">
|
||||||
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, ", ");
|
|
||||||
}
|
|
||||||
</cfscript>
|
|
||||||
|
|
||||||
<cfif structKeyExists(url, "geocode") AND structKeyExists(url, "addressId")>
|
<cfif structKeyExists(url, "geocode") AND structKeyExists(url, "addressId")>
|
||||||
<cfset addressId = val(url.addressId)>
|
<cfset addressId = val(url.addressId)>
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ try {
|
||||||
zip: zip,
|
zip: zip,
|
||||||
addrID: qAddr.ID
|
addrID: qAddr.ID
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
geocodeAddrId = qAddr.ID;
|
||||||
} else {
|
} else {
|
||||||
// Create new address
|
// Create new address
|
||||||
queryTimed("
|
queryTimed("
|
||||||
|
|
@ -123,8 +124,18 @@ try {
|
||||||
zip: zip,
|
zip: zip,
|
||||||
bizID: businessId
|
bizID: businessId
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
qNewAddr = queryTimed("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||||
|
geocodeAddrId = qNewAddr.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-geocode address in background
|
||||||
|
</cfscript>
|
||||||
|
<cfthread action="run" name="geocode_biz_#geocodeAddrId#" addressId="#geocodeAddrId#">
|
||||||
|
<cfinclude template="/api/inc/geocode.cfm">
|
||||||
|
<cfset geocodeAddressById(attributes.addressId)>
|
||||||
|
</cfthread>
|
||||||
|
<cfscript>
|
||||||
|
|
||||||
response.OK = true;
|
response.OK = true;
|
||||||
|
|
||||||
} catch (any e) {
|
} catch (any e) {
|
||||||
|
|
|
||||||
58
api/inc/geocode.cfm
Normal file
58
api/inc/geocode.cfm
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<cfscript>
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
@ -129,6 +129,15 @@ try {
|
||||||
if (arrayLen(addrUpdates) > 0) {
|
if (arrayLen(addrUpdates) > 0) {
|
||||||
queryTimed("UPDATE Addresses SET " & arrayToList(addrUpdates, ", ") & " WHERE ID = :addrId",
|
queryTimed("UPDATE Addresses SET " & arrayToList(addrUpdates, ", ") & " WHERE ID = :addrId",
|
||||||
addrParams, { datasource: "payfrit" });
|
addrParams, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Auto-geocode address in background
|
||||||
|
geocodeAddrId = qExistingAddr.ID;
|
||||||
|
</cfscript>
|
||||||
|
<cfthread action="run" name="geocode_settings_#geocodeAddrId#" addressId="#geocodeAddrId#">
|
||||||
|
<cfinclude template="/api/inc/geocode.cfm">
|
||||||
|
<cfset geocodeAddressById(attributes.addressId)>
|
||||||
|
</cfthread>
|
||||||
|
<cfscript>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,14 @@ try {
|
||||||
addressId = qNewAddr.id;
|
addressId = qNewAddr.id;
|
||||||
response.steps.append("Created address record (ID: " & addressId & ")");
|
response.steps.append("Created address record (ID: " & addressId & ")");
|
||||||
|
|
||||||
|
// Auto-geocode address in background
|
||||||
|
</cfscript>
|
||||||
|
<cfthread action="run" name="geocode_#addressId#" addressId="#addressId#">
|
||||||
|
<cfinclude template="/api/inc/geocode.cfm">
|
||||||
|
<cfset geocodeAddressById(attributes.addressId)>
|
||||||
|
</cfthread>
|
||||||
|
<cfscript>
|
||||||
|
|
||||||
// Get community meal type (1=provide meals, 2=food bank donation)
|
// Get community meal type (1=provide meals, 2=food bank donation)
|
||||||
communityMealType = structKeyExists(wizardData, "communityMealType") && isSimpleValue(wizardData.communityMealType) ? val(wizardData.communityMealType) : 1;
|
communityMealType = structKeyExists(wizardData, "communityMealType") && isSimpleValue(wizardData.communityMealType) ? val(wizardData.communityMealType) : 1;
|
||||||
if (communityMealType < 1 || communityMealType > 2) communityMealType = 1;
|
if (communityMealType < 1 || communityMealType > 2) communityMealType = 1;
|
||||||
|
|
|
||||||
Reference in a new issue