Browse Source

Add geocode endpoint

master
Skylar Ittner 2 months ago
parent
commit
c0c89ec8f7
  1. 15
      apiconfig.php
  2. 9
      composer.json
  3. 2209
      composer.lock
  4. 91
      endpoints/gis.geocode.php
  5. 471
      lib/StreetNormalizer.lib.php
  6. 157
      lib/geoutil.lib.php

15
apiconfig.php

@ -12,14 +12,13 @@ $APIS = [
"vars" => [
]
],
// "gis/geocode" => [
// "load" => "gis.geocode.php",
// "vars" => [
// "latitude" => "/\-?[0-9]{1,3}(\.[0-9]{0,10})?/",
// "longitude" => "/\-?[0-9]{1,3}(\.[0-9]{0,10})?/",
// "nocache (optional)" => ""
// ]
// ],
"gis/geocode" => [
"load" => "gis.geocode.php",
"vars" => [
"address" => "",
"nocache (optional)" => ""
]
],
"gis/geocode/reverse" => [
"load" => "gis.geocode.reverse.php",
"vars" => [

9
composer.json

@ -6,6 +6,13 @@
"shippo/shippo-php": "^1.4",
"easypost/easypost-php": "^3.5",
"bogdaan/open-location-code": "dev-master",
"singpolyma/openpgp-php": "^0.5.0"
"singpolyma/openpgp-php": "^0.5.0",
"geocoder-php/chain-provider": "^4.3",
"geocoder-php/mapbox-provider": "^1.3",
"geocoder-php/mapquest-provider": "^4.2",
"guzzlehttp/psr7": "^2.1",
"php-http/curl-client": "^2.2",
"guzzlehttp/guzzle": "^7.4",
"php-http/guzzle7-adapter": "^1.0"
}
}

2209
composer.lock
File diff suppressed because it is too large
View File

91
endpoints/gis.geocode.php

@ -0,0 +1,91 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
use Geocoder\Query\GeocodeQuery;
$address = urldecode($VARS["address"]);
$origaddress = $address;
$cacheresp = $memcache->get("gis.geocode." . sha1($origaddress));
if ($cacheresp !== false && empty($VARS["nocache"])) {
exitWithJson(json_decode($cacheresp, true));
}
$geocoder = new \Geocoder\ProviderAggregator();
$adapter = new \Http\Adapter\Guzzle7\Client();
$chain = new \Geocoder\Provider\Chain\Chain([
new \Geocoder\Provider\Mapbox\Mapbox($adapter, env("mapbox_key")),
new \Geocoder\Provider\MapQuest\MapQuest($adapter, env("mapquest_key"))
]);
$geocoder->registerProvider($chain);
$query = GeocodeQuery::create($address)->withLimit(1);
if (isset($VARS["country"]) && preg_match("/^[A-Z]{2}$/", $VARS["country"])) {
$query = $query->withData("country", $VARS["country"]);
}
$results = $geocoder->geocodeQuery($query);
if ($results->count() > 0) {
$result = $results->first();
} else {
$output = [
"status" => "ERROR",
"message" => "Address not found.",
"address" => [
"original" => $origaddress,
"street" => "",
"postalCode" => ""
],
"coords" => [
0,
0
],
"accuracy" => [
"ok" => false
],
"provider" => ""
];
goto cacheout;
}
$geolatitude = $result->getCoordinates()->getLatitude();
$geolongitude = $result->getCoordinates()->getLongitude();
$accurate = !empty($result->getStreetNumber());
if ($accurate) {
$address = implode(" ", [$result->getStreetNumber(), $result->getStreetName()]);
} else {
$address = $result->getStreetName();
}
$output = [
"status" => "OK",
"address" => [
"original" => $origaddress,
"street" => ucwords(strtolower(StreetNormalizer::normalizeAddress($address))),
"postalCode" => $result->getPostalCode()
],
"coords" => [
$geolatitude,
$geolongitude
],
"accuracy" => [
"ok" => $accurate
],
"provider" => $result->getProvidedBy()
];
cacheout:
$memcache->set("gis.geocode." . sha1($origaddress), json_encode($output));
exitWithJson($output);

471
lib/StreetNormalizer.lib.php

@ -0,0 +1,471 @@
<?php
class StreetNormalizer {
/*
* Machine-readable form of the chart at
* https://pe.usps.com/text/pub28/28apc_002.htm
* with loop->lp added
*/
const SUFFIX_TABLE = [
'ALLEE' => 'ALY',
'ALLEY' => 'ALY',
'ALLY' => 'ALY',
'ANEX' => 'ANX',
'ANNEX' => 'ANX',
'ANNX' => 'ANX',
'ARC ' => 'ARC',
'ARCADE ' => 'ARC',
'AV' => 'AVE',
'AVEN' => 'AVE',
'AVENU' => 'AVE',
'AVENUE' => 'AVE',
'AVN' => 'AVE',
'AVNUE' => 'AVE',
'BAYOO' => 'BYU',
'BAYOU' => 'BYU',
'BEACH' => 'BCH',
'BEND' => 'BND',
'BLUF' => 'BLF',
'BLUFF' => 'BLF',
'BLUFFS ' => 'BLFS',
'BOT' => 'BTM',
'BOTTM' => 'BTM',
'BOTTOM' => 'BTM',
'BOUL' => 'BLVD',
'BOULEVARD ' => 'BLVD',
'BOULV' => 'BLVD',
'BRNCH' => 'BR',
'BRANCH' => 'BR',
'BRDGE' => 'BRG',
'BRIDGE' => 'BRG',
'BROOK' => 'BRK',
'BROOKS ' => 'BRKS',
'BURG' => 'BG',
'BURGS' => 'BGS',
'BYPA' => 'BYP',
'BYPAS' => 'BYP',
'BYPASS' => 'BYP',
'BYPS' => 'BYP',
'CAMP' => 'CP',
'CMP' => 'CP',
'CANYN' => 'CYN',
'CANYON' => 'CYN',
'CNYN' => 'CYN',
'CAPE' => 'CPE',
'CAUSEWAY' => 'CSWY',
'CAUSWA' => 'CSWY',
'CEN' => 'CTR',
'CENT' => 'CTR',
'CENTER' => 'CTR',
'CENTR' => 'CTR',
'CENTRE' => 'CTR',
'CNTER' => 'CTR',
'CNTR' => 'CTR',
'CENTERS ' => 'CTRS',
'CIRC' => 'CIR',
'CIRCL' => 'CIR',
'CIRCLE' => 'CIR',
'CRCL' => 'CIR',
'CRCLE' => 'CIR',
'CIRCLES' => 'CIRS',
'CLIFF' => 'CLF',
'CLIFFS' => 'CLFS',
'CLUB' => 'CLB',
'COMMON' => 'CMN',
'COMMONS' => 'CMNS',
'CORNER' => 'COR',
'CORNERS' => 'CORS',
'COURSE' => 'CRSE',
'COURT' => 'CT',
'COURTS' => 'CTS',
'COVE' => 'CV',
'COVES' => 'CVS',
'CREEK' => 'CRK',
'CRESCENT' => 'CRES',
'CRSENT' => 'CRES',
'CRSNT' => 'CRES',
'CREST' => 'CRST',
'CROSSING ' => 'XING',
'CRSSNG ' => 'XING',
'XING ' => 'XING',
'CROSSROAD' => 'XRD',
'CROSSROADS' => 'XRDS',
'CURVE ' => 'CURV',
'DALE ' => 'DL',
'DL ' => 'DL',
'DAM ' => 'DM',
'DM ' => 'DM',
'DIV' => 'DV',
'DIVIDE' => 'DV',
'DVD' => 'DV',
'DRIV' => 'DR',
'DRIVE' => 'DR',
'DRV' => 'DR',
'DRIVES' => 'DRS',
'ESTATE' => 'EST',
'ESTATES' => 'ESTS',
'EXP' => 'EXPY',
'EXPR' => 'EXPY',
'EXPRESS' => 'EXPY',
'EXPRESSWAY' => 'EXPY',
'EXPW' => 'EXPY',
'EXTENSION' => 'EXT',
'EXTN' => 'EXT',
'EXTNSN' => 'EXT',
'FALLS' => 'FLS',
'FERRY' => 'FRY',
'FRRY' => 'FRY',
'FIELD' => 'FLD',
'FIELDS' => 'FLDS',
'FLAT' => 'FLT',
'FLATS' => 'FLTS',
'FORD' => 'FRD',
'FORDS' => 'FRDS',
'FOREST' => 'FRST',
'FORESTS' => 'FRST',
'FORG' => 'FRG',
'FORGE' => 'FRG',
'FORGES' => 'FRGS',
'FORK' => 'FRK',
'FORKS' => 'FRKS',
'FORT' => 'FT',
'FRT' => 'FT',
'FREEWAY' => 'FWY',
'FREEWY' => 'FWY',
'FRWAY' => 'FWY',
'FRWY' => 'FWY',
'GARDEN' => 'GDN',
'GARDN' => 'GDN',
'GRDEN' => 'GDN',
'GRDN' => 'GDN',
'GARDENS' => 'GDNS',
'GRDNS' => 'GDNS',
'GATEWAY' => 'GTWY',
'GATEWY' => 'GTWY',
'GATWAY' => 'GTWY',
'GTWAY' => 'GTWY',
'GLEN' => 'GLN',
'GLENS' => 'GLNS',
'GREEN' => 'GRN',
'GREENS' => 'GRNS',
'GROV' => 'GRV',
'GROVE' => 'GRV',
'GROVES' => 'GRVS',
'HARB' => 'HBR',
'HARBOR' => 'HBR',
'HARBR' => 'HBR',
'HRBOR' => 'HBR',
'HARBORS' => 'HBRS',
'HAVEN' => 'HVN',
'HT' => 'HTS',
'HIGHWAY' => 'HWY',
'HIGHWY' => 'HWY',
'HIWAY' => 'HWY',
'HIWY' => 'HWY',
'HWAY' => 'HWY',
'HILL' => 'HL',
'HILLS' => 'HLS',
'HLLW' => 'HOLW',
'HOLLOW' => 'HOLW',
'HOLLOWS' => 'HOLW',
'HOLWS' => 'HOLW',
'ISLAND' => 'IS',
'ISLND' => 'IS',
'ISLANDS' => 'ISS',
'ISLNDS' => 'ISS',
'ISLES' => 'ISLE',
'JCTION' => 'JCT',
'JCTN' => 'JCT',
'JUNCTION' => 'JCT',
'JUNCTN' => 'JCT',
'JUNCTON' => 'JCT',
'JCTNS' => 'JCTS',
'JUNCTIONS' => 'JCTS',
'KEY' => 'KY',
'KEYS' => 'KYS',
'KNL' => 'KNL',
'KNOL' => 'KNL',
'KNOLL' => 'KNL',
'KNOLLS' => 'KNLS',
'LAKE' => 'LK',
'LAKES' => 'LKS',
'LANDING' => 'LNDG',
'LNDNG' => 'LNDG',
'LANE' => 'LN',
'LIGHT' => 'LGT',
'LIGHTS' => 'LGTS',
'LOAF' => 'LF',
'LOCK' => 'LCK',
'LOCKS' => 'LCKS',
'LDGE' => 'LDG',
'LODG' => 'LDG',
'LODGE' => 'LDG',
'MANOR' => 'MNR',
'MANORS' => 'MNRS',
'MEADOW' => 'MDW',
'MDW' => 'MDWS',
'MEADOWS' => 'MDWS',
'MEDOWS' => 'MDWS',
'MILL' => 'ML',
'MILLS' => 'MLS',
'MISSN' => 'MSN',
'MSSN' => 'MSN',
'MOTORWAY' => 'MTWY',
'MNT' => 'MT',
'MOUNT' => 'MT',
'MNTAIN' => 'MTN',
'MNTN' => 'MTN',
'MOUNTAIN' => 'MTN',
'MOUNTIN' => 'MTN',
'MTIN' => 'MTN',
'MNTNS' => 'MTNS',
'MOUNTAINS' => 'MTNS',
'NECK' => 'NCK',
'ORCHARD' => 'ORCH',
'ORCHRD' => 'ORCH',
'OVL' => 'OVAL',
'OVERPASS' => 'OPAS',
'PRK' => 'PARK',
'PARKS' => 'PARK',
'PARKWAY' => 'PKWY',
'PARKWY' => 'PKWY',
'PKWAY' => 'PKWY',
'PKY' => 'PKWY',
'PARKWAYS' => 'PKWY',
'PKWYS' => 'PKWY',
'PASSAGE' => 'PSGE',
'PATHS' => 'PATH',
'PIKES' => 'PIKE',
'PINE' => 'PNE',
'PINES' => 'PNES',
'PLAIN' => 'PLN',
'PLAINS' => 'PLNS',
'PLAZA' => 'PLZ',
'PLZA' => 'PLZ',
'POINT' => 'PT',
'POINTS' => 'PTS',
'PORT' => 'PRT',
'PORTS' => 'PRTS',
'PRAIRIE' => 'PR',
'PRR' => 'PR',
'RAD' => 'RADL',
'RADIAL' => 'RADL',
'RADIEL' => 'RADL',
'RANCH' => 'RNCH',
'RANCHES' => 'RNCH',
'RNCHS' => 'RNCH',
'RAPID' => 'RPD',
'RAPIDS' => 'RPDS',
'REST' => 'RST',
'RDGE' => 'RDG',
'RIDGE' => 'RDG',
'RIDGES' => 'RDGS',
'RIVER' => 'RIV',
'RVR' => 'RIV',
'RIVR' => 'RIV',
'ROAD' => 'RD',
'ROADS' => 'RDS',
'ROUTE' => 'RTE',
'SHOAL' => 'SHL',
'SHOALS' => 'SHLS',
'SHOAR' => 'SHR',
'SHORE' => 'SHR',
'SHOARS' => 'SHRS',
'SHORES' => 'SHRS',
'SKYWAY' => 'SKWY',
'SPNG' => 'SPG',
'SPRING' => 'SPG',
'SPRNG' => 'SPG',
'SPNGS' => 'SPGS',
'SPRINGS' => 'SPGS',
'SPRNGS' => 'SPGS',
'SPURS' => 'SPUR',
'SQR' => 'SQ',
'SQRE' => 'SQ',
'SQU' => 'SQ',
'SQUARE' => 'SQ',
'SQRS' => 'SQS',
'SQUARES' => 'SQS',
'STATION' => 'STA',
'STATN' => 'STA',
'STN' => 'STA',
'STRAV' => 'STRA',
'STRAVEN' => 'STRA',
'STRAVENUE' => 'STRA',
'STRAVN' => 'STRA',
'STRVN' => 'STRA',
'STRVNUE' => 'STRA',
'STREAM' => 'STRM',
'STREME' => 'STRM',
'STREET' => 'ST',
'STRT' => 'ST',
'STR' => 'ST',
'STREETS' => 'STS',
'SUMIT' => 'SMT',
'SUMITT' => 'SMT',
'SUMMIT' => 'SMT',
'TERR' => 'TER',
'TERRACE' => 'TER',
'THROUGHWAY' => 'TRWY',
'TRACE' => 'TRCE',
'TRACES' => 'TRCE',
'TRACK' => 'TRAK',
'TRACKS' => 'TRAK',
'TRK' => 'TRAK',
'TRKS' => 'TRAK',
'TRAFFICWAY' => 'TRFY',
'TRAIL' => 'TRL',
'TRAILS' => 'TRL',
'TRLS' => 'TRL',
'TRAILER' => 'TRLR',
'TRLRS' => 'TRLR',
'TUNEL' => 'TUNL',
'TUNLS' => 'TUNL',
'TUNNEL' => 'TUNL',
'TUNNELS' => 'TUNL',
'TUNNL' => 'TUNL',
'TRNPK' => 'TPKE',
'TURNPIKE' => 'TPKE',
'TURNPK' => 'TPKE',
'UNDERPASS' => 'UPAS',
'UNION' => 'UN',
'UNIONS' => 'UNS',
'VALLEY' => 'VLY',
'VALLY' => 'VLY',
'VLLY' => 'VLY',
'VALLEYS' => 'VLYS',
'VDCT' => 'VIA',
'VIADCT' => 'VIA',
'VIADUCT' => 'VIA',
'VIEW' => 'VW',
'VIEWS' => 'VWS',
'VILL' => 'VLG',
'VILLAG' => 'VLG',
'VILLAGE' => 'VLG',
'VILLG' => 'VLG',
'VILLIAGE' => 'VLG',
'VILLAGES' => 'VLGS',
'VILLE' => 'VL',
'VIST' => 'VIS',
'VISTA' => 'VIS',
'VST' => 'VIS',
'VSTA' => 'VIS',
'WALKS' => 'WALK',
'WY' => 'WAY',
'WELL' => 'WL',
'WELLS' => 'WLS'
];
const CARDINAL_TABLE = [
"NORTH" => "N",
"SOUTH" => "S",
"EAST" => "E",
"WEST" => "W",
"NORTHWEST" => "NW",
"SOUTHWEST" => "SW",
"NORTHEAST" => "NE",
"SOUTHEAST" => "SE"
];
/**
* Normalize a street name (ex. Street Road)
* @param string $street
* @param bool $python Set to false to use built-in less-accurate code,
* or true to use normalize.py and https://github.com/mcmire/address_standardization
* @return string
*/
public static function normalizeStreet(string $street, bool $python = true): string {
// Give the script a dummy house number so it doesn't get lost
$filler_address = "10000001 ";
return str_replace($filler_address, "", static::normalizeAddress($filler_address . $street));
}
/**
* Normalize an address line (ex. 1234 street road)
* @param string $address
* @param bool $python Set to false to use built-in less-accurate code,
* or true to use normalize.py and https://github.com/mcmire/address_standardization
* @return string
* @throws Exception
*/
public static function normalizeAddress(string $address, bool $python = true): string {
global $SETTINGS;
try {
if (empty($SETTINGS["normalize_python_script"]) || !file_exists($SETTINGS["normalize_python_script"])) {
throw new Exception("failing back to builtin: python script missing");
}
if (!$python) {
throw new Exception("failing back to builtin due to user request");
}
$escaped = escapeshellarg($address);
$json = shell_exec($SETTINGS["normalize_python_script"] . " " . $escaped);
$address = json_decode($json, true);
if (empty($address)) {
throw new Exception("failing back to builtin due to JSON error");
}
$address = $address["address_line_1"];
return $address;
} catch (Exception $ex) {
$address = strtoupper(trim($address));
$address = static::normalizeSuffix($address);
$address = static::normalizeCardinals($address);
return $address;
}
}
private static function findReplace(string $str, array $lookuptable) {
foreach ($lookuptable as $find => $replace) {
if ($str == $find) {
$str = $replace;
break;
}
}
return $str;
}
private static function findReplaceAll(string $str, array $lookuptable, string $seperator = " ") {
$words = explode($seperator, $str);
for ($i = 0; $i < count($words); $i++) {
$words[$i] = static::findReplace($words[$i], $lookuptable);
}
return implode($seperator, $words);
}
/**
* Replace cardinal/compass directions with abbreviations
* @param string $street
* @return string
*/
public static function normalizeCardinals(string $street): string {
$street = strtoupper(trim($street));
return static::findReplaceAll($street, static::CARDINAL_TABLE);
}
/**
* Replace the street suffix with the standard abbreviation.
* @param string $street
* @return string
*/
public static function normalizeSuffix(string $street): string {
$street = strtoupper(trim($street));
$parts = explode(" ", $street);
$suffix = static::findReplace(end($parts), static::SUFFIX_TABLE);
$parts[count($parts) - 1] = $suffix;
return implode(" ", $parts);
}
}

157
lib/geoutil.lib.php

@ -0,0 +1,157 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
function parse_citystatezip($citystatezip) {
$citystatezip = strtoupper(trim($citystatezip));
$fullregex = "/^(.+), ?(.+)[,| |, ]([0-9]{5})(-[0-9]{4})?$/";
$citystateregex = "/^(\w+)[,\s]+([A-Z]{2,})+$/";
if (!preg_match($fullregex, $citystatezip, $matches)) {
if (!preg_match($citystateregex, $citystatezip, $matches)) {
return null;
}
}
return [
"city" => $matches[1],
"state" => $matches[2],
"zip" => isset($matches[3]) ? $matches[3] : null
];
}
function parse_citystate($citystate) {
$citystate = strtoupper(trim($citystate));
$regex = "/^(\w+)[,\s]+([A-Z]{2,})+$/";
if (!preg_match($regex, $citystate, $matches)) {
return null;
}
return [
"city" => $matches[1],
"state" => $matches[2]
];
}
function geocode_returnresult($query) {
global $SETTINGS, $database;
$geocoder = new \Geocoder\ProviderAggregator();
$adapter = new \Http\Adapter\Guzzle6\Client();
$chain = new \Geocoder\Provider\Chain\Chain([
new \Geocoder\Provider\LocalDatabase\LocalDatabase($database),
new \Geocoder\Provider\Mapbox\Mapbox($adapter, $SETTINGS["mapbox_key"]),
new \Geocoder\Provider\MapQuest\MapQuest($adapter, $SETTINGS["mapquest_key"])
]);
$geocoder->registerProvider($chain);
$results = $geocoder->geocodeQuery($query);
if ($results->count() > 0) {
$result = $results->first();
} else {
header("Content-Type: application/json");
return [
"status" => "ERROR",
"message" => "That address doesn't seem to exist. If this is an error, please send the full address (including city, state, and ZIP) to support@netsyms.com.",
"address" => [
"street" => "",
"postalCode" => ""
],
"coords" => [
0,
0
],
"accuracy" => [
"ok" => false
],
"provider" => ""
];
}
$geolatitude = $result->getCoordinates()->getLatitude();
$geolongitude = $result->getCoordinates()->getLongitude();
$accurate = !empty($result->getStreetNumber());
if ($accurate) {
$address = implode(" ", [$result->getStreetNumber(), $result->getStreetName()]);
} else {
$address = $result->getStreetName();
}
return [
"status" => "OK",
"address" => [
"street" => ucwords(strtolower(StreetNormalizer::normalizeAddress($address))),
"postalCode" => $result->getPostalCode()
],
"coords" => [
$geolatitude,
$geolongitude
],
"accuracy" => [
"ok" => $accurate
],
"provider" => $result->getProvidedBy()
];
}
function geocode_addrstring($address) {
$query = Geocoder\Query\GeocodeQuery::create($address)->withLimit(1);
return geocode_returnresult($query);
}
/**
* Geocode an address.
* @global type $SETTINGS
* @param type $number
* @param type $street
* @param type $unit
* @param type $city
* @param type $state
* @param type $zip
* @param type $country
* @param type $type
* @return type
*/
function geocode($number, $street, $unit = null, $city, $state, $zip = null, $country = null, $type = null) {
$address = "$number $street $city $state";
$origaddress = "$number $street";
$query = Geocoder\Query\GeocodeQuery::create($address)
->withLimit(1)
->withData("number", $number)
->withData("street", $street)
->withData("city", $city)
->withData("state", $state);
if (!is_null($zip) && preg_match("/^([0-9]{5})/", $zip, $matches)) {
if (count($matches) > 0) {
$query = $query->withData("zip", $matches[0]);
}
}
if (!is_null($unit) && !empty($unit)) {
$query = $query->withData("unit", $unit);
}
if (!is_null($type) && count(array_diff(explode("|", $type), $SETTINGS["address_types"])) == 0) {
$query = $query->withData("locationtype", $type);
}
if (!is_null($country) && preg_match("/^[A-Z]{2}$/", $country)) {
$query = $query->withData("country", $country);
}
return geocode_returnresult($query);
}
Loading…
Cancel
Save