X-Git-Url: https://git.openstreetmap.org./nominatim.git/blobdiff_plain/09f16616388490578ddebdb1d8be5ae545cd1d64..8e0ffde3e0a403d289b91ad720444fd94f3ae210:/lib/Geocode.php diff --git a/lib/Geocode.php b/lib/Geocode.php index 971eb85d..58bc3bfc 100644 --- a/lib/Geocode.php +++ b/lib/Geocode.php @@ -5,6 +5,7 @@ namespace Nominatim; require_once(CONST_BasePath.'/lib/NearPoint.php'); require_once(CONST_BasePath.'/lib/PlaceLookup.php'); require_once(CONST_BasePath.'/lib/ReverseGeocode.php'); +require_once(CONST_BasePath.'/lib/SearchDescription.php'); class Geocode { @@ -51,10 +52,22 @@ class Geocode protected $sQuery = false; protected $aStructuredQuery = false; + protected $oNormalizer = null; + public function __construct(&$oDB) { $this->oDB =& $oDB; + $this->oNormalizer = \Transliterator::createFromRules(CONST_Term_Normalization_Rules); + } + + private function normTerm($sTerm) + { + if ($this->oNormalizer === null) { + return $sTerm; + } + + return $this->oNormalizer->transliterate($sTerm); } public function setReverseInPlan($bReverse) @@ -292,7 +305,7 @@ class Geocode $aViewbox = $oParams->getStringList('viewboxlbrt'); if ($aViewbox) { if (count($aViewbox) != 4) { - userError("Bad parmater 'viewbox'. Expected 4 coordinates."); + userError("Bad parmater 'viewboxlbrt'. Expected 4 coordinates."); } $this->setViewbox($aViewbox); } else { @@ -301,12 +314,7 @@ class Geocode if (count($aViewbox) != 4) { userError("Bad parmater 'viewbox'. Expected 4 coordinates."); } - $this->setViewBox(array( - $aViewbox[0], - $aViewbox[3], - $aViewbox[2], - $aViewbox[1] - )); + $this->setViewBox($aViewbox); } else { $aRoute = $oParams->getStringList('route'); $fRouteWidth = $oParams->getFloat('routewidth'); @@ -360,7 +368,7 @@ class Geocode $this->aAddressRankList = array(); $this->aStructuredQuery = array(); - $this->sAllowedTypesSQLList = ''; + $this->sAllowedTypesSQLList = false; $this->loadStructuredAddressElement($sAmenity, 'amenity', 26, 30, false); $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false); @@ -373,7 +381,7 @@ class Geocode if (sizeof($this->aStructuredQuery) > 0) { $this->sQuery = join(', ', $this->aStructuredQuery); if ($this->iMaxAddressRank < 30) { - $sAllowedTypesSQLList = '(\'place\',\'boundary\')'; + $this->sAllowedTypesSQLList = '(\'place\',\'boundary\')'; } } } @@ -404,14 +412,23 @@ class Geocode //$aPlaceIDs is an array with key: placeID and value: tiger-housenumber, if found, else -1 if (sizeof($aPlaceIDs) == 0) return array(); - $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]"; + $sLanguagePrefArraySQL = getArraySQL( + array_map("getDBQuoted", $this->aLangPrefOrder) + ); // Get the details for display (is this a redundant extra step?) $sPlaceIDs = join(',', array_keys($aPlaceIDs)); $sImportanceSQL = ''; - if ($this->sViewboxSmallSQL) $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * "; - if ($this->sViewboxLargeSQL) $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * "; + $sImportanceSQLGeom = ''; + if ($this->sViewboxSmallSQL) { + $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * "; + $sImportanceSQLGeom .= " CASE WHEN ST_Contains($this->sViewboxSmallSQL, geometry) THEN 1 ELSE 0.75 END * "; + } + if ($this->sViewboxLargeSQL) { + $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * "; + $sImportanceSQLGeom .= " CASE WHEN ST_Contains($this->sViewboxLargeSQL, geometry) THEN 1 ELSE 0.75 END * "; + } $sSQL = "SELECT "; $sSQL .= " osm_type,"; @@ -423,7 +440,7 @@ class Geocode $sSQL .= " rank_address,"; $sSQL .= " min(place_id) AS place_id, "; $sSQL .= " min(parent_place_id) AS parent_place_id, "; - $sSQL .= " calculated_country_code AS country_code, "; + $sSQL .= " country_code, "; $sSQL .= " get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) AS langaddress,"; $sSQL .= " get_name_by_language(name, $sLanguagePrefArraySQL) AS placename,"; $sSQL .= " get_name_by_language(name, ARRAY['ref']) AS ref,"; @@ -466,7 +483,7 @@ class Geocode $sSQL .= " admin_level, "; $sSQL .= " rank_search, "; $sSQL .= " rank_address, "; - $sSQL .= " calculated_country_code, "; + $sSQL .= " country_code, "; $sSQL .= " importance, "; if (!$this->bDeDupe) $sSQL .= "place_id,"; $sSQL .= " langaddress, "; @@ -476,6 +493,35 @@ class Geocode if ($this->bIncludeNameDetails) $sSQL .= "name, "; $sSQL .= " extratags->'place' "; + // postcode table + $sSQL .= "UNION "; + $sSQL .= "SELECT"; + $sSQL .= " 'P' as osm_type,"; + $sSQL .= " (SELECT osm_id from placex p WHERE p.place_id = lp.parent_place_id) as osm_id,"; + $sSQL .= " 'place' as class, 'postcode' as type,"; + $sSQL .= " null as admin_level, rank_search, rank_address,"; + $sSQL .= " place_id, parent_place_id, country_code,"; + $sSQL .= " get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) AS langaddress,"; + $sSQL .= " postcode as placename,"; + $sSQL .= " postcode as ref,"; + if ($this->bIncludeExtraTags) $sSQL .= "null AS extra,"; + if ($this->bIncludeNameDetails) $sSQL .= "null AS names,"; + $sSQL .= " ST_x(st_centroid(geometry)) AS lon, ST_y(st_centroid(geometry)) AS lat,"; + $sSQL .= $sImportanceSQLGeom."(0.75-(rank_search::float/40)) AS importance, "; + $sSQL .= " ("; + $sSQL .= " SELECT max(p.importance*(p.rank_address+2))"; + $sSQL .= " FROM "; + $sSQL .= " place_addressline s, "; + $sSQL .= " placex p"; + $sSQL .= " WHERE s.place_id = lp.parent_place_id"; + $sSQL .= " AND p.place_id = s.address_place_id "; + $sSQL .= " AND s.isaddress"; + $sSQL .= " AND p.importance is not null"; + $sSQL .= " ) AS addressimportance, "; + $sSQL .= " null AS extra_place "; + $sSQL .= "FROM location_postcode lp"; + $sSQL .= " WHERE place_id in ($sPlaceIDs) "; + if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank) { // only Tiger housenumbers and interpolation lines need to be interpolated, because they are saved as lines // with start- and endnumber, the common osm housenumbers are usually saved as points @@ -551,7 +597,7 @@ class Geocode $sSQL .= " 30 AS rank_address, "; $sSQL .= " min(place_id) as place_id, "; $sSQL .= " min(parent_place_id) AS parent_place_id, "; - $sSQL .= " calculated_country_code AS country_code, "; + $sSQL .= " country_code, "; $sSQL .= " get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) AS langaddress, "; $sSQL .= " null AS placename, "; $sSQL .= " null AS ref, "; @@ -576,7 +622,7 @@ class Geocode $sSQL .= " SELECT "; $sSQL .= " osm_id, "; $sSQL .= " place_id, "; - $sSQL .= " calculated_country_code, "; + $sSQL .= " country_code, "; $sSQL .= " CASE "; // interpolate the housenumbers here $sSQL .= " WHEN startnumber != endnumber "; $sSQL .= " THEN ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) "; @@ -595,7 +641,7 @@ class Geocode $sSQL .= " osm_id, "; $sSQL .= " place_id, "; $sSQL .= " housenumber_for_place, "; - $sSQL .= " calculated_country_code "; //is this group by really needed?, place_id + housenumber (in combination) are unique + $sSQL .= " country_code "; //is this group by really needed?, place_id + housenumber (in combination) are unique if (!$this->bDeDupe) $sSQL .= ", place_id "; if (CONST_Use_Aux_Location_data) { @@ -653,7 +699,7 @@ class Geocode return $aSearchResults; } - public function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases) + public function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases, $sNormQuery) { /* Calculate all searches using aValidTokens i.e. @@ -666,12 +712,17 @@ class Geocode Score how good the search is so they can be ordered */ - foreach ($aPhrases as $iPhrase => $sPhrase) { + $iGlobalRank = 0; + + foreach ($aPhrases as $iPhrase => $aPhrase) { $aNewPhraseSearches = array(); - if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase]; - else $sPhraseType = ''; + if ($bStructuredPhrases) { + $sPhraseType = $aPhraseTypes[$iPhrase]; + } else { + $sPhraseType = ''; + } - foreach ($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset) { + foreach ($aPhrase['wordsets'] as $iWordSet => $aWordset) { // Too many permutations - too expensive if ($iWordSet > 120) break; @@ -682,167 +733,72 @@ class Geocode //echo "
$sToken"; $aNewWordsetSearches = array(); - foreach ($aWordsetSearches as $aCurrentSearch) { + foreach ($aWordsetSearches as $oCurrentSearch) { //echo ""; - //var_dump($aCurrentSearch); + //var_dump($oCurrentSearch); //echo ""; // If the token is valid if (isset($aValidTokens[' '.$sToken])) { foreach ($aValidTokens[' '.$sToken] as $aSearchTerm) { - $aSearch = $aCurrentSearch; - $aSearch['iSearchRank']++; - if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0') { - if ($aSearch['sCountryCode'] === false) { - $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']); - // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation) - if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases))) { - $aSearch['iSearchRank'] += 5; - } - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - } - } elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null) { - if ($aSearch['oNear'] === false) { - $aSearch['oNear'] = new NearPoint( - $aSearchTerm['lat'], - $aSearchTerm['lon'], - $aSearchTerm['radius'] - ); - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - } - } elseif ($sPhraseType == 'postalcode') { - // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both - if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) { - // If we already have a name try putting the postcode first - if (sizeof($aSearch['aName'])) { - $aNewSearch = $aSearch; - $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']); - $aNewSearch['aName'] = array(); - $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch; - } - - if (sizeof($aSearch['aName'])) { - if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) { - $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - } else { - $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - $aSearch['iSearchRank'] += 1000; // skip; - } - } else { - $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - //$aSearch['iNamePhrase'] = $iPhrase; - } - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - } - } elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house') { - if ($aSearch['sHouseNumber'] === '') { - $aSearch['sHouseNumber'] = $sToken; - // sanity check: if the housenumber is not mainly made - // up of numbers, add a penalty - if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++; - // also housenumbers should appear in the first or second phrase - if ($iPhrase > 1) $aSearch['iSearchRank'] += 1; - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - /* - // Fall back to not searching for this item (better than nothing) - $aSearch = $aCurrentSearch; - $aSearch['iSearchRank'] += 1; - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - */ - } - } elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null) { - if ($aSearch['sClass'] === '') { - $aSearch['sOperator'] = $aSearchTerm['operator']; - $aSearch['sClass'] = $aSearchTerm['class']; - $aSearch['sType'] = $aSearchTerm['type']; - if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name'; - else $aSearch['sOperator'] = 'near'; // near = in for the moment - if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1; - - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - } - } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) { - if (sizeof($aSearch['aName'])) { - if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) { - $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - } else { - $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - $aSearch['iSearchRank'] += 1000; // skip; - } - } else { - $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - //$aSearch['iNamePhrase'] = $iPhrase; + // Recheck if the original word shows up in the query. + $bWordInQuery = false; + if (isset($aSearchTerm['word']) && $aSearchTerm['word']) { + $bWordInQuery = strpos( + $sNormQuery, + $this->normTerm($aSearchTerm['word']) + ) !== false; + } + $aNewSearches = $oCurrentSearch->extendWithFullTerm( + $aSearchTerm, + $bWordInQuery, + isset($aValidTokens[$sToken]) + && strpos($sToken, ' ') === false, + $sPhraseType, + $iToken == 0 && $iPhrase == 0, + $iPhrase == 0, + $iToken + 1 == sizeof($aWordset) + && $iPhrase + 1 == sizeof($aPhrases), + $iGlobalRank + ); + + foreach ($aNewSearches as $oSearch) { + if ($oSearch->getRank() < $this->iMaxRank) { + $aNewWordsetSearches[] = $oSearch; } - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; } } } // Look for partial matches. // Note that there is no point in adding country terms here - // because country are omitted in the address. + // because country is omitted in the address. if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country') { // Allow searching for a word - but at extra cost foreach ($aValidTokens[$sToken] as $aSearchTerm) { - if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) { - if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false) { - $aSearch = $aCurrentSearch; - $aSearch['iSearchRank'] += 1; - if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) { - $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - } elseif (isset($aValidTokens[' '.$sToken])) { // revert to the token version? - $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - $aSearch['iSearchRank'] += 1; - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - foreach ($aValidTokens[' '.$sToken] as $aSearchTermToken) { - if (empty($aSearchTermToken['country_code']) - && empty($aSearchTermToken['lat']) - && empty($aSearchTermToken['class']) - ) { - $aSearch = $aCurrentSearch; - $aSearch['iSearchRank'] += 1; - $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id']; - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - } - } - } else { - $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2; - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; - } - } - - if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase) { - $aSearch = $aCurrentSearch; - $aSearch['iSearchRank'] += 1; - if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1; - if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2; - if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) { - $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - } else { - $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id']; - } - $aSearch['iNamePhrase'] = $iPhrase; - if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch; + $aNewSearches = $oCurrentSearch->extendWithPartialTerm( + $aSearchTerm, + $bStructuredPhrases, + $iPhrase, + $aWordFrequencyScores, + isset($aValidTokens[' '.$sToken]) ? $aValidTokens[' '.$sToken] : array() + ); + + foreach ($aNewSearches as $oSearch) { + if ($oSearch->getRank() < $this->iMaxRank) { + $aNewWordsetSearches[] = $oSearch; } } } - } else { - // Allow skipping a word - but at EXTREAM cost - //$aSearch = $aCurrentSearch; - //$aSearch['iSearchRank']+=100; - //$aNewWordsetSearches[] = $aSearch; } } // Sort and cut - usort($aNewWordsetSearches, 'bySearchRank'); + usort($aNewWordsetSearches, array('Nominatim\SearchDescription', 'bySearchRank')); $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50); } //var_Dump('
',sizeof($aWordsetSearches)); exit; $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches); - usort($aNewPhraseSearches, 'bySearchRank'); + usort($aNewPhraseSearches, array('Nominatim\SearchDescription', 'bySearchRank')); $aSearchHash = array(); foreach ($aNewPhraseSearches as $iSearch => $aSearch) { @@ -857,9 +813,12 @@ class Geocode // Re-group the searches by their score, junk anything over 20 as just not worth trying $aGroupedSearches = array(); foreach ($aNewPhraseSearches as $aSearch) { - if ($aSearch['iSearchRank'] < $this->iMaxRank) { - if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array(); - $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch; + $iRank = $aSearch->getRank(); + if ($iRank < $this->iMaxRank) { + if (!isset($aGroupedSearches[$iRank])) { + $aGroupedSearches[$iRank] = array(); + } + $aGroupedSearches[$iRank][] = $aSearch; } } ksort($aGroupedSearches); @@ -874,6 +833,22 @@ class Geocode //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens); } + + // Revisit searches, drop bad searches and give penalty to unlikely combinations. + $aGroupedSearches = array(); + foreach ($aSearches as $oSearch) { + if (!$oSearch->isValidSearch($this->aCountryCodes)) { + continue; + } + + $iRank = $oSearch->addToRank($iGlobalRank); + if (!isset($aGroupedSearches[$iRank])) { + $aGroupedSearches[$iRank] = array(); + } + $aGroupedSearches[$iRank][] = $oSearch; + } + ksort($aGroupedSearches); + return $aGroupedSearches; } @@ -913,7 +888,10 @@ class Geocode { if (!$this->sQuery && !$this->aStructuredQuery) return array(); - $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]"; + $sNormQuery = $this->normTerm($this->sQuery); + $sLanguagePrefArraySQL = getArraySQL( + array_map("getDBQuoted", $this->aLangPrefOrder) + ); $sCountryCodesSQL = false; if ($this->aCountryCodes) { $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes)); @@ -956,70 +934,59 @@ class Geocode $aSearchResults = array(); if ($sQuery || $this->aStructuredQuery) { - // Start with a blank search - $aSearches = array( - array( - 'iSearchRank' => 0, - 'iNamePhrase' => -1, - 'sCountryCode' => false, - 'aName' => array(), - 'aAddress' => array(), - 'aFullNameAddress' => array(), - 'aNameNonSearch' => array(), - 'aAddressNonSearch' => array(), - 'sOperator' => '', - 'aFeatureName' => array(), - 'sClass' => '', - 'sType' => '', - 'sHouseNumber' => '', - 'oNear' => $oNearPoint - ) - ); - - // Any 'special' terms in the search? - $bSpecialTerms = false; - preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER); - $aSpecialTerms = array(); - foreach ($aSpecialTermsRaw as $aSpecialTerm) { - $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery); - $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2]; + // Start with a single blank search + $aSearches = array(new SearchDescription()); + + if ($oNearPoint) { + $aSearches[0]->setNear($oNearPoint); + } + + if ($sQuery) { + $sQuery = $aSearches[0]->extractKeyValuePairs($sQuery); } - preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER); - $aSpecialTerms = array(); - if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity']) { - $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']); + $sSpecialTerm = ''; + if ($sQuery) { + preg_match_all( + '/\\[([\\w ]*)\\]/u', + $sQuery, + $aSpecialTermsRaw, + PREG_SET_ORDER + ); + foreach ($aSpecialTermsRaw as $aSpecialTerm) { + $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery); + if (!$sSpecialTerm) { + $sSpecialTerm = $aSpecialTerm[1]; + } + } + } + if (!$sSpecialTerm && $this->aStructuredQuery + && isset($this->aStructuredQuery['amenity'])) { + $sSpecialTerm = $this->aStructuredQuery['amenity']; unset($this->aStructuredQuery['amenity']); } - foreach ($aSpecialTermsRaw as $aSpecialTerm) { - $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery); - $sToken = chksql($this->oDB->getOne("SELECT make_standard_name('".$aSpecialTerm[1]."') AS string")); - $sSQL = 'SELECT * '; - $sSQL .= 'FROM ( '; - $sSQL .= ' SELECT word_id, word_token, word, class, type, country_code, operator'; - $sSQL .= ' FROM word '; + if ($sSpecialTerm && !$aSearches[0]->hasOperator()) { + $sSpecialTerm = pg_escape_string($sSpecialTerm); + $sToken = chksql( + $this->oDB->getOne("SELECT make_standard_name('$sSpecialTerm')"), + "Cannot decode query. Wrong encoding?" + ); + $sSQL = 'SELECT class, type FROM word '; $sSQL .= ' WHERE word_token in (\' '.$sToken.'\')'; - $sSQL .= ') AS x '; - $sSQL .= ' WHERE (class is not null AND class not in (\'place\')) '; - $sSQL .= ' OR country_code is not null'; + $sSQL .= ' AND class is not null AND class not in (\'place\')'; if (CONST_Debug) var_Dump($sSQL); $aSearchWords = chksql($this->oDB->getAll($sSQL)); $aNewSearches = array(); - foreach ($aSearches as $aSearch) { + foreach ($aSearches as $oSearch) { foreach ($aSearchWords as $aSearchTerm) { - $aNewSearch = $aSearch; - if ($aSearchTerm['country_code']) { - $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']); - $aNewSearches[] = $aNewSearch; - $bSpecialTerms = true; - } - if ($aSearchTerm['class']) { - $aNewSearch['sClass'] = $aSearchTerm['class']; - $aNewSearch['sType'] = $aSearchTerm['type']; - $aNewSearches[] = $aNewSearch; - $bSpecialTerms = true; - } + $oNewSearch = clone $oSearch; + $oNewSearch->setPoiSearch( + Operator::TYPE, + $aSearchTerm['class'], + $aSearchTerm['type'] + ); + $aNewSearches[] = $oNewSearch; } } $aSearches = $aNewSearches; @@ -1068,14 +1035,10 @@ class Geocode if (CONST_Debug) var_Dump($sSQL); $aValidTokens = array(); - if (sizeof($aTokens)) { - $aDatabaseWords = chksql( - $this->oDB->getAll($sSQL), - "Could not get word tokens." - ); - } else { - $aDatabaseWords = array(); - } + $aDatabaseWords = chksql( + $this->oDB->getAll($sSQL), + "Could not get word tokens." + ); $aPossibleMainWordIDs = array(); $aWordFrequencyScores = array(); foreach ($aDatabaseWords as $aToken) { @@ -1096,21 +1059,9 @@ class Geocode } if (CONST_Debug) var_Dump($aPhrases, $aValidTokens); - // Try and calculate GB postcodes we might be missing + // US ZIP+4 codes - if there is no token, merge in the 5-digit ZIP code foreach ($aTokens as $sToken) { - // Source of gb postcodes is now definitive - always use - if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData)) { - if (substr($aData[1], -2, 1) != ' ') { - $aData[0] = substr($aData[0], 0, strlen($aData[1])-1).' '.substr($aData[0], strlen($aData[1])-1); - $aData[1] = substr($aData[1], 0, -1).' '.substr($aData[1], -1, 1); - } - $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB); - if ($aGBPostcodeLocation) { - $aValidTokens[$sToken] = $aGBPostcodeLocation; - } - } elseif (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData)) { - // US ZIP+4 codes - if there is no token, - // merge in the 5-digit ZIP code + if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData)) { if (isset($aValidTokens[$aData[1]])) { foreach ($aValidTokens[$aData[1]] as $aToken) { if (!$aToken['class']) { @@ -1127,8 +1078,8 @@ class Geocode foreach ($aTokens as $sToken) { // Unknown single word token with a number - assume it is a house number - if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken, ' ') === false && preg_match('/[0-9]/', $sToken)) { - $aValidTokens[' '.$sToken] = array(array('class' => 'place', 'type' => 'house')); + if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken, ' ') === false && preg_match('/^[0-9]+$/', $sToken)) { + $aValidTokens[' '.$sToken] = array(array('class' => 'place', 'type' => 'house', 'word_token' => ' '.$sToken)); } } @@ -1139,7 +1090,7 @@ class Geocode // array with: placeid => -1 | tiger-housenumber $aResultPlaceIDs = array(); - $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases); + $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases, $sNormQuery); if ($this->bReverseInPlan) { // Reverse phrase array and also reverse the order of the wordsets in @@ -1151,14 +1102,14 @@ class Geocode $aFinalPhrase = end($aPhrases); $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0); } - $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false); + $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false, $sNormQuery); foreach ($aGroupedSearches as $aSearches) { foreach ($aSearches as $aSearch) { - if ($aSearch['iSearchRank'] < $this->iMaxRank) { - if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array(); - $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch; + if (!isset($aReverseGroupedSearches[$aSearch->getRank()])) { + $aReverseGroupedSearches[$aSearch->getRank()] = array(); } + $aReverseGroupedSearches[$aSearch->getRank()][] = $aSearch; } } @@ -1169,39 +1120,9 @@ class Geocode // Re-group the searches by their score, junk anything over 20 as just not worth trying $aGroupedSearches = array(); foreach ($aSearches as $aSearch) { - if ($aSearch['iSearchRank'] < $this->iMaxRank) { - if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array(); - $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch; - } - } - ksort($aGroupedSearches); - } - - if (CONST_Debug) var_Dump($aGroupedSearches); - if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0) { - $aCopyGroupedSearches = $aGroupedSearches; - foreach ($aCopyGroupedSearches as $iGroup => $aSearches) { - foreach ($aSearches as $iSearch => $aSearch) { - $aReductionsList = array($aSearch['aAddress']); - $iSearchRank = $aSearch['iSearchRank']; - while (sizeof($aReductionsList) > 0) { - $iSearchRank += 5; - if ($iSearchRank > iMaxRank) break 3; - $aNewReductionsList = array(); - foreach ($aReductionsList as $aReductionsWordList) { - for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++) { - $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1)); - $aReverseSearch = $aSearch; - $aSearch['aAddress'] = $aReductionsWordListResult; - $aSearch['iSearchRank'] = $iSearchRank; - $aGroupedSearches[$iSearchRank][] = $aReverseSearch; - if (sizeof($aReductionsWordListResult) > 0) { - $aNewReductionsList[] = $aReductionsWordListResult; - } - } - } - $aReductionsList = $aNewReductionsList; - } + if ($aSearch->getRank() < $this->iMaxRank) { + if (!isset($aGroupedSearches[$aSearch->getRank()])) $aGroupedSearches[$aSearch->getRank()] = array(); + $aGroupedSearches[$aSearch->getRank()][] = $aSearch; } } ksort($aGroupedSearches); @@ -1227,420 +1148,89 @@ class Geocode $iQueryLoop = 0; foreach ($aGroupedSearches as $iGroupedRank => $aSearches) { $iGroupLoop++; - foreach ($aSearches as $aSearch) { + foreach ($aSearches as $oSearch) { $iQueryLoop++; $searchedHousenumber = -1; if (CONST_Debug) echo "
Search Loop, group $iGroupLoop, loop $iQueryLoop"; - if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens); - - // No location term? - if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['oNear']) { - if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber']) { - // Just looking for a country by code - look it up - if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank) { - $sSQL = "SELECT place_id FROM placex WHERE calculated_country_code='".$aSearch['sCountryCode']."' AND rank_search = 4"; - if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)"; - if ($bBoundingBoxSearch) - $sSQL .= " AND _st_intersects($this->sViewboxSmallSQL, geometry)"; - $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1"; - if (CONST_Debug) var_dump($sSQL); - $aPlaceIDs = chksql($this->oDB->getCol($sSQL)); - } else { - $aPlaceIDs = array(); - } - } else { - if (!$bBoundingBoxSearch && !$aSearch['oNear']) continue; - if (!$aSearch['sClass']) continue; - - $sSQL = "SELECT COUNT(*) FROM pg_tables WHERE tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'"; - if (chksql($this->oDB->getOne($sSQL))) { - $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct"; - if ($sCountryCodesSQL) $sSQL .= " JOIN placex USING (place_id)"; - $sSQL .= " WHERE st_contains($this->sViewboxSmallSQL, ct.centroid)"; - if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)"; - if (sizeof($this->aExcludePlaceIDs)) { - $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")"; - } - if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC"; - $sSQL .= " limit $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - $aPlaceIDs = chksql($this->oDB->getCol($sSQL)); - - // If excluded place IDs are given, it is fair to assume that - // there have been results in the small box, so no further - // expansion in that case. - // Also don't expand if bounded results were requested. - if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch) { - $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct"; - if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)"; - $sSQL .= " WHERE ST_Contains($this->sViewboxLargeSQL, ct.centroid)"; - if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)"; - if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC"; - $sSQL .= " LIMIT $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - $aPlaceIDs = chksql($this->oDB->getCol($sSQL)); - } - } else { - $sSQL = "SELECT place_id "; - $sSQL .= "FROM placex "; - $sSQL .= "WHERE class='".$aSearch['sClass']."' "; - $sSQL .= " AND type='".$aSearch['sType']."'"; - $sSQL .= " AND ST_Contains($this->sViewboxSmallSQL, geometry) "; - $sSQL .= " AND linked_place_id is null"; - if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)"; - if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, centroid) ASC"; - $sSQL .= " LIMIT $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - $aPlaceIDs = chksql($this->oDB->getCol($sSQL)); - } - } - } elseif ($aSearch['oNear'] && !sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['sClass']) { - // If a coordinate is given, the search must either - // be for a name or a special search. Ignore everythin else. - $aPlaceIDs = array(); - } else { - $aPlaceIDs = array(); - - // First we need a position, either aName or fLat or both - $aTerms = array(); - $aOrder = array(); - - if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress'])) { - $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M'; - $aOrder[] = ""; - $aOrder[0] = " ("; - $aOrder[0] .= " EXISTS("; - $aOrder[0] .= " SELECT place_id "; - $aOrder[0] .= " FROM placex "; - $aOrder[0] .= " WHERE parent_place_id = search_name.place_id"; - $aOrder[0] .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."' "; - $aOrder[0] .= " LIMIT 1"; - $aOrder[0] .= " ) "; - // also housenumbers from interpolation lines table are needed - $aOrder[0] .= " OR EXISTS("; - $aOrder[0] .= " SELECT place_id "; - $aOrder[0] .= " FROM location_property_osmline "; - $aOrder[0] .= " WHERE parent_place_id = search_name.place_id"; - $aOrder[0] .= " AND startnumber is not NULL"; - $aOrder[0] .= " AND ".intval($aSearch['sHouseNumber']).">=startnumber "; - $aOrder[0] .= " AND ".intval($aSearch['sHouseNumber'])."<=endnumber "; - $aOrder[0] .= " LIMIT 1"; - $aOrder[0] .= " )"; - $aOrder[0] .= " )"; - $aOrder[0] .= " DESC"; - } - - // TODO: filter out the pointless search terms (2 letter name tokens and less) - // they might be right - but they are just too darned expensive to run - if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'], ",")."]"; - if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'], ",")."]"; - if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress']) { - // For infrequent name terms disable index usage for address - if (CONST_Search_NameOnlySearchFrequencyThreshold - && sizeof($aSearch['aName']) == 1 - && $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold - ) { - $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'], $aSearch['aAddressNonSearch']), ",")."]"; - } else { - $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'], ",")."]"; - if (sizeof($aSearch['aAddressNonSearch'])) { - $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'], ",")."]"; - } - } - } - if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'"; - if ($aSearch['sHouseNumber']) { - $aTerms[] = "address_rank between 16 and 27"; - } else { - if ($this->iMinAddressRank > 0) { - $aTerms[] = "address_rank >= ".$this->iMinAddressRank; - } - if ($this->iMaxAddressRank < 30) { - $aTerms[] = "address_rank <= ".$this->iMaxAddressRank; - } - } - if ($aSearch['oNear']) { - $aTerms[] = $aSearch['oNear']->withinSQL('centroid'); - - $aOrder[] = $aSearch['oNear']->distanceSQL('centroid'); - } - if (sizeof($this->aExcludePlaceIDs)) { - $aTerms[] = "place_id not in (".join(',', $this->aExcludePlaceIDs).")"; - } - if ($sCountryCodesSQL) { - $aTerms[] = "country_code in ($sCountryCodesSQL)"; - } - - if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL"; - if ($oNearPoint) { - $aOrder[] = $oNearPoint->distanceSQL('centroid'); - } - - if ($aSearch['sHouseNumber']) { - $sImportanceSQL = '- abs(26 - address_rank) + 3'; - } else { - $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)'; + if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($oSearch)), $aValidTokens); + + $aPlaceIDs = array(); + if ($oSearch->isCountrySearch()) { + // Just looking for a country - look it up + if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank) { + $aPlaceIDs = $oSearch->queryCountry( + $this->oDB, + $bBoundingBoxSearch ? $this->sViewboxSmallSQL : '' + ); } - if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END"; - if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END"; - - $aOrder[] = "$sImportanceSQL DESC"; - if (sizeof($aSearch['aFullNameAddress'])) { - $sExactMatchSQL = ' ( '; - $sExactMatchSQL .= ' SELECT count(*) FROM ( '; - $sExactMatchSQL .= ' SELECT unnest(ARRAY['.join($aSearch['aFullNameAddress'], ",").']) '; - $sExactMatchSQL .= ' INTERSECT '; - $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)'; - $sExactMatchSQL .= ' ) s'; - $sExactMatchSQL .= ') as exactmatch'; - $aOrder[] = 'exactmatch DESC'; - } else { - $sExactMatchSQL = '0::int as exactmatch'; + } elseif (!$oSearch->isNamedSearch()) { + // looking for a POI in a geographic area + if (!$bBoundingBoxSearch && !$oSearch->isNearSearch()) { + continue; } - if (sizeof($aTerms)) { - $sSQL = "SELECT place_id, "; - $sSQL .= $sExactMatchSQL; - $sSQL .= " FROM search_name"; - $sSQL .= " WHERE ".join(' and ', $aTerms); - $sSQL .= " ORDER BY ".join(', ', $aOrder); - if ($aSearch['sHouseNumber'] || $aSearch['sClass']) { - $sSQL .= " LIMIT 20"; - } elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass']) { - $sSQL .= " LIMIT 1"; - } else { - $sSQL .= " LIMIT ".$this->iLimit; - } - - if (CONST_Debug) var_dump($sSQL); - $aViewBoxPlaceIDs = chksql( - $this->oDB->getAll($sSQL), - "Could not get places for search terms." - ); - //var_dump($aViewBoxPlaceIDs); - // Did we have an viewbox matches? - $aPlaceIDs = array(); - $bViewBoxMatch = false; - foreach ($aViewBoxPlaceIDs as $aViewBoxRow) { - //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break; - //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break; - //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1; - //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2; - $aPlaceIDs[] = $aViewBoxRow['place_id']; - $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch']; + $aPlaceIDs = $oSearch->queryNearbyPoi( + $this->oDB, + $sCountryCodesSQL, + $bBoundingBoxSearch ? $this->sViewboxSmallSQL : '', + $this->sViewboxCentreSQL, + $this->aExcludePlaceIDs ? join(',', $this->aExcludePlaceIDs) : '', + $this->iLimit + ); + } elseif ($oSearch->isOperator(Operator::POSTCODE)) { + $aPlaceIDs = $oSearch->queryPostcode( + $this->oDB, + $sCountryCodesSQL, + $this->iLimit + ); + } else { + // Ordinary search: + // First search for places according to name and address. + $aNamedPlaceIDs = $oSearch->queryNamedPlace( + $this->oDB, + $aWordFrequencyScores, + $sCountryCodesSQL, + $this->iMinAddressRank, + $this->iMaxAddressRank, + $this->aExcludePlaceIDs ? join(',', $this->aExcludePlaceIDs) : '', + $bBoundingBoxSearch ? $this->sViewboxSmallSQL : '', + $bBoundingBoxSearch ? $this->sViewboxLargeSQL : '', + $this->iLimit + ); + + if (sizeof($aNamedPlaceIDs)) { + foreach ($aNamedPlaceIDs as $aRow) { + $aPlaceIDs[] = $aRow['place_id']; + $this->exactMatchCache[$aRow['place_id']] = $aRow['exactmatch']; } } - //var_Dump($aPlaceIDs); - //exit; //now search for housenumber, if housenumber provided - if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs)) { - $searchedHousenumber = intval($aSearch['sHouseNumber']); - $aRoadPlaceIDs = $aPlaceIDs; - $sPlaceIDs = join(',', $aPlaceIDs); - - // Now they are indexed, look for a house attached to a street we found - $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M'; - $sSQL = "SELECT place_id FROM placex "; - $sSQL .= "WHERE parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'"; - if (sizeof($this->aExcludePlaceIDs)) { - $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")"; - } - $sSQL .= " LIMIT $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - $aPlaceIDs = chksql($this->oDB->getCol($sSQL)); - - // if nothing found, search in the interpolation line table - if (!sizeof($aPlaceIDs)) { - // do we need to use transliteration and the regex for housenumbers??? - //new query for lines, not housenumbers anymore - $sSQL = "SELECT distinct place_id FROM location_property_osmline"; - $sSQL .= " WHERE startnumber is not NULL and parent_place_id in (".$sPlaceIDs.") and ("; - if ($searchedHousenumber%2 == 0) { - //if housenumber is even, look for housenumber in streets with interpolationtype even or all - $sSQL .= "interpolationtype='even'"; - } else { - //look for housenumber in streets with interpolationtype odd or all - $sSQL .= "interpolationtype='odd'"; - } - $sSQL .= " or interpolationtype='all') and "; - $sSQL .= $searchedHousenumber.">=startnumber and "; - $sSQL .= $searchedHousenumber."<=endnumber"; - - if (sizeof($this->aExcludePlaceIDs)) { - $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")"; - } - //$sSQL .= " limit $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - //get place IDs - $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0)); - } - - // If nothing found try the aux fallback table - if (CONST_Use_Aux_Location_data && !sizeof($aPlaceIDs)) { - $sSQL = "SELECT place_id FROM location_property_aux "; - $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") "; - $sSQL .= " AND housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'"; - if (sizeof($this->aExcludePlaceIDs)) { - $sSQL .= " AND parent_place_id not in (".join(',', $this->aExcludePlaceIDs).")"; - } - //$sSQL .= " limit $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - $aPlaceIDs = chksql($this->oDB->getCol($sSQL)); - } - - //if nothing was found in placex or location_property_aux, then search in Tiger data for this housenumber(location_property_tiger) - if (CONST_Use_US_Tiger_Data && !sizeof($aPlaceIDs)) { - $sSQL = "SELECT distinct place_id FROM location_property_tiger"; - $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") and ("; - if ($searchedHousenumber%2 == 0) { - $sSQL .= "interpolationtype='even'"; - } else { - $sSQL .= "interpolationtype='odd'"; - } - $sSQL .= " or interpolationtype='all') and "; - $sSQL .= $searchedHousenumber.">=startnumber and "; - $sSQL .= $searchedHousenumber."<=endnumber"; - - if (sizeof($this->aExcludePlaceIDs)) { - $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")"; - } - //$sSQL .= " limit $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - //get place IDs - $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0)); - } + if ($oSearch->hasHouseNumber() && sizeof($aPlaceIDs)) { + $aResult = $oSearch->queryHouseNumber( + $this->oDB, + $aPlaceIDs, + $this->aExcludePlaceIDs ? join(',', $this->aExcludePlaceIDs) : '', + $this->iLimit + ); - // Fallback to the road (if no housenumber was found) - if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber'])) { - $aPlaceIDs = $aRoadPlaceIDs; - //set to -1, if no housenumbers were found - $searchedHousenumber = -1; + if (sizeof($aResult)) { + $searchedHousenumber = $aResult['iHouseNumber']; + $aPlaceIDs = $aResult['aPlaceIDs']; + } elseif (!$oSearch->looksLikeFullAddress()) { + $aPlaceIDs = array(); } - //else: housenumber was found, remains saved in searchedHousenumber } - - if ($aSearch['sClass'] && sizeof($aPlaceIDs)) { - $sPlaceIDs = join(',', $aPlaceIDs); - $aClassPlaceIDs = array(); - - if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name') { - // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match - $sSQL = "SELECT place_id "; - $sSQL .= " FROM placex "; - $sSQL .= " WHERE place_id in ($sPlaceIDs) "; - $sSQL .= " AND class='".$aSearch['sClass']."' "; - $sSQL .= " AND type='".$aSearch['sType']."'"; - $sSQL .= " AND linked_place_id is null"; - if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)"; - $sSQL .= " ORDER BY rank_search ASC "; - $sSQL .= " LIMIT $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - $aClassPlaceIDs = chksql($this->oDB->getCol($sSQL)); - } - - if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') { // & in - $sSQL = "SELECT count(*) FROM pg_tables "; - $sSQL .= "WHERE tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'"; - $bCacheTable = chksql($this->oDB->getOne($sSQL)); - - $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)"; - - if (CONST_Debug) var_dump($sSQL); - $this->iMaxRank = ((int)chksql($this->oDB->getOne($sSQL))); - - // For state / country level searches the normal radius search doesn't work very well - $sPlaceGeom = false; - if ($this->iMaxRank < 9 && $bCacheTable) { - // Try and get a polygon to search in instead - $sSQL = "SELECT geometry "; - $sSQL .= " FROM placex"; - $sSQL .= " WHERE place_id in ($sPlaceIDs)"; - $sSQL .= " AND rank_search < $this->iMaxRank + 5"; - $sSQL .= " AND ST_Geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon')"; - $sSQL .= " ORDER BY rank_search ASC "; - $sSQL .= " LIMIT 1"; - if (CONST_Debug) var_dump($sSQL); - $sPlaceGeom = chksql($this->oDB->getOne($sSQL)); - } - - if ($sPlaceGeom) { - $sPlaceIDs = false; - } else { - $this->iMaxRank += 5; - $sSQL = "SELECT place_id FROM placex WHERE place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank"; - if (CONST_Debug) var_dump($sSQL); - $aPlaceIDs = chksql($this->oDB->getCol($sSQL)); - $sPlaceIDs = join(',', $aPlaceIDs); - } - - if ($sPlaceIDs || $sPlaceGeom) { - $fRange = 0.01; - if ($bCacheTable) { - // More efficient - can make the range bigger - $fRange = 0.05; - - $sOrderBySQL = ''; - if ($oNearPoint) { - $sOrderBySQL = $oNearPoint->distanceSQL('l.centroid'); - } elseif ($sPlaceIDs) { - $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)"; - } elseif ($sPlaceGeom) { - $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)"; - } - - $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l"; - if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)"; - if ($sPlaceIDs) { - $sSQL .= ",placex as f where "; - $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) "; - } - if ($sPlaceGeom) { - $sSQL .= " where "; - $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) "; - } - if (sizeof($this->aExcludePlaceIDs)) { - $sSQL .= " and l.place_id not in (".join(',', $this->aExcludePlaceIDs).")"; - } - if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)"; - if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc"; - if ($this->iOffset) $sSQL .= " offset $this->iOffset"; - $sSQL .= " limit $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL))); - } else { - if ($aSearch['oNear']) { - $fRange = $aSearch['oNear']->radius(); - } - - $sOrderBySQL = ''; - if ($oNearPoint) { - $sOrderBySQL = $oNearPoint->distanceSQL('l.geometry'); - } else { - $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)"; - } - - $sSQL = "SELECT distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:''); - $sSQL .= " FROM placex as l, placex as f "; - $sSQL .= " WHERE f.place_id in ($sPlaceIDs) "; - $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange) "; - $sSQL .= " AND l.class='".$aSearch['sClass']."' "; - $sSQL .= " AND l.type='".$aSearch['sType']."' "; - if (sizeof($this->aExcludePlaceIDs)) { - $sSQL .= " AND l.place_id not in (".join(',', $this->aExcludePlaceIDs).")"; - } - if ($sCountryCodesSQL) $sSQL .= " AND l.calculated_country_code in ($sCountryCodesSQL)"; - if ($sOrderBy) $sSQL .= "ORDER BY ".$OrderBysSQL." ASC"; - if ($this->iOffset) $sSQL .= " OFFSET $this->iOffset"; - $sSQL .= " limit $this->iLimit"; - if (CONST_Debug) var_dump($sSQL); - $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL))); - } - } - } - $aPlaceIDs = $aClassPlaceIDs; + // finally get POIs if requested + if ($oSearch->isPoiSearch() && sizeof($aPlaceIDs)) { + $aPlaceIDs = $oSearch->queryPoiByOperator( + $this->oDB, + $aPlaceIDs, + $this->aExcludePlaceIDs ? join(',', $this->aExcludePlaceIDs) : '', + $this->iLimit + ); } } @@ -1649,6 +1239,21 @@ class Geocode var_Dump($aPlaceIDs); } + if (sizeof($aPlaceIDs) && $oSearch->getPostcode()) { + $sSQL = 'SELECT place_id FROM placex'; + $sSQL .= ' WHERE place_id in ('.join(',', $aPlaceIDs).')'; + $sSQL .= " AND postcode = '".$oSearch->getPostcode()."'"; + if (CONST_Debug) var_dump($sSQL); + $aFilteredPlaceIDs = chksql($this->oDB->getCol($sSQL)); + if ($aFilteredPlaceIDs) { + $aPlaceIDs = $aFilteredPlaceIDs; + if (CONST_Debug) { + echo "
Place IDs after postcode filtering: "; + var_Dump($aPlaceIDs); + } + } + } + foreach ($aPlaceIDs as $iPlaceID) { // array for placeID => -1 | Tiger housenumber $aResultPlaceIDs[$iPlaceID] = $searchedHousenumber; @@ -1660,9 +1265,11 @@ class Geocode // Need to verify passes rank limits before dropping out of the loop (yuk!) // reduces the number of place ids, like a filter // rank_address is 30 for interpolated housenumbers + $sWherePlaceId = 'WHERE place_id in ('; + $sWherePlaceId .= join(',', array_keys($aResultPlaceIDs)).') '; + $sSQL = "SELECT place_id "; - $sSQL .= "FROM placex "; - $sSQL .= "WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).") "; + $sSQL .= "FROM placex ".$sWherePlaceId; $sSQL .= " AND ("; $sSQL .= " placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank "; if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) { @@ -1671,20 +1278,23 @@ class Geocode if ($this->aAddressRankList) { $sSQL .= " OR placex.rank_address in (".join(',', $this->aAddressRankList).")"; } - if (CONST_Use_US_Tiger_Data) { - $sSQL .= " ) "; + $sSQL .= " ) UNION "; + $sSQL .= " SELECT place_id FROM location_postcode lp ".$sWherePlaceId; + $sSQL .= " AND (lp.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank "; + if ($this->aAddressRankList) { + $sSQL .= " OR lp.rank_address in (".join(',', $this->aAddressRankList).")"; + } + $sSQL .= ") "; + if (CONST_Use_US_Tiger_Data && $this->iMaxAddressRank == 30) { + $sSQL .= "UNION "; + $sSQL .= " SELECT place_id "; + $sSQL .= " FROM location_property_tiger ".$sWherePlaceId; + } + if ($this->iMaxAddressRank == 30) { $sSQL .= "UNION "; $sSQL .= " SELECT place_id "; - $sSQL .= " FROM location_property_tiger "; - $sSQL .= " WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).") "; - $sSQL .= " AND (30 between $this->iMinAddressRank and $this->iMaxAddressRank "; - if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',', $this->aAddressRankList).")"; + $sSQL .= " FROM location_property_osmline ".$sWherePlaceId; } - $sSQL .= ") UNION "; - $sSQL .= " SELECT place_id "; - $sSQL .= " FROM location_property_osmline "; - $sSQL .= " WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).")"; - $sSQL .= " AND startnumber is not NULL AND (30 between $this->iMinAddressRank and $this->iMaxAddressRank)"; if (CONST_Debug) var_dump($sSQL); $aFilteredPlaceIDs = chksql($this->oDB->getCol($sSQL)); $tempIDs = array();