function lookup() { if (!$this->iPlaceID) { return null; } $sLanguagePrefArraySQL = "ARRAY[" . join(',', array_map("getDBQuoted", $this->aLangPrefOrder)) . "]"; $sSQL = "select placex.place_id, partition, osm_type, osm_id, class, type, admin_level, housenumber, street, isin, postcode, country_code, extratags, parent_place_id, linked_place_id, rank_address, rank_search, "; $sSQL .= " coalesce(importance,0.75-(rank_search::float/40)) as importance, indexed_status, indexed_date, wikipedia, calculated_country_code, "; $sSQL .= " get_address_by_language(place_id, {$sLanguagePrefArraySQL}) as langaddress,"; $sSQL .= " get_name_by_language(name, {$sLanguagePrefArraySQL}) as placename,"; $sSQL .= " get_name_by_language(name, ARRAY['ref']) as ref,"; $sSQL .= " (case when centroid is null then st_y(st_centroid(geometry)) else st_y(centroid) end) as lat,"; $sSQL .= " (case when centroid is null then st_x(st_centroid(geometry)) else st_x(centroid) end) as lon"; $sSQL .= " from placex where place_id = " . (int) $this->iPlaceID; if ($this->bIsTiger) { $sSQL = "select place_id,partition, 'T' as osm_type, place_id as osm_id, 'place' as class, 'house' as type, null as admin_level, housenumber, null as street, null as isin, postcode,"; $sSQL .= " 'us' as country_code, null as extratags, parent_place_id, null as linked_place_id, 30 as rank_address, 30 as rank_search,"; $sSQL .= " coalesce(null,0.75-(30::float/40)) as importance, null as indexed_status, null as indexed_date, null as wikipedia, 'us' as calculated_country_code, "; $sSQL .= " get_address_by_language(place_id, {$sLanguagePrefArraySQL}) as langaddress,"; $sSQL .= " null as placename,"; $sSQL .= " null as ref,"; $sSQL .= " st_y(centroid) as lat,"; $sSQL .= " st_x(centroid) as lon"; $sSQL .= " from location_property_tiger where place_id = " . (int) $this->iPlaceID; } $aPlace = $this->oDB->getRow($sSQL); if (PEAR::IsError($aPlace)) { failInternalError("Could not lookup place.", $sSQL, $aPlace); } if (!$aPlace['place_id']) { return null; } if ($this->bAddressDetails) { $aAddress = $this->getAddressNames(); $aPlace['aAddress'] = $aAddress; } $aClassType = getClassTypes(); $sAddressType = ''; $sClassType = $aPlace['class'] . ':' . $aPlace['type'] . ':' . $aPlace['admin_level']; if (isset($aClassType[$sClassType]) && isset($aClassType[$sClassType]['simplelabel'])) { $sAddressType = $aClassType[$aClassType]['simplelabel']; } else { $sClassType = $aPlace['class'] . ':' . $aPlace['type']; if (isset($aClassType[$sClassType]) && isset($aClassType[$sClassType]['simplelabel'])) { $sAddressType = $aClassType[$sClassType]['simplelabel']; } else { $sAddressType = $aPlace['class']; } } $aPlace['addresstype'] = $sAddressType; return $aPlace; }
function lookup() { if (!$this->sQuery && !$this->aStructuredQuery) { return false; } $sLanguagePrefArraySQL = "ARRAY[" . join(',', array_map("getDBQuoted", $this->aLangPrefOrder)) . "]"; $sCountryCodesSQL = false; if ($this->aCountryCodes && sizeof($this->aCountryCodes)) { $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes)); } $sQuery = $this->sQuery; // Conflicts between US state abreviations and various words for 'the' in different languages if (isset($this->aLangPrefOrder['name:en'])) { $sQuery = preg_replace('/(^|,)\\s*il\\s*(,|$)/', '\\1illinois\\2', $sQuery); $sQuery = preg_replace('/(^|,)\\s*al\\s*(,|$)/', '\\1alabama\\2', $sQuery); $sQuery = preg_replace('/(^|,)\\s*la\\s*(,|$)/', '\\1louisiana\\2', $sQuery); } // View Box SQL $sViewboxCentreSQL = false; $bBoundingBoxSearch = false; if ($this->aViewBox) { $fHeight = $this->aViewBox[0] - $this->aViewBox[2]; $fWidth = $this->aViewBox[1] - $this->aViewBox[3]; $aBigViewBox[0] = $this->aViewBox[0] + $fHeight; $aBigViewBox[2] = $this->aViewBox[2] - $fHeight; $aBigViewBox[1] = $this->aViewBox[1] + $fWidth; $aBigViewBox[3] = $this->aViewBox[3] - $fWidth; $this->sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(" . (double) $this->aViewBox[0] . "," . (double) $this->aViewBox[1] . "),ST_Point(" . (double) $this->aViewBox[2] . "," . (double) $this->aViewBox[3] . ")),4326)"; $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(" . (double) $aBigViewBox[0] . "," . (double) $aBigViewBox[1] . "),ST_Point(" . (double) $aBigViewBox[2] . "," . (double) $aBigViewBox[3] . ")),4326)"; $bBoundingBoxSearch = $this->bBoundedSearch; } // Route SQL if ($this->aRoutePoints) { $sViewboxCentreSQL = "ST_SetSRID('LINESTRING("; $bFirst = true; foreach ($this->aRoutePoints as $aPoint) { if (!$bFirst) { $sViewboxCentreSQL .= ","; } $sViewboxCentreSQL .= $aPoint[0] . ' ' . $aPoint[1]; $bFirst = false; } $sViewboxCentreSQL .= ")'::geometry,4326)"; $sSQL = "select st_buffer(" . $sViewboxCentreSQL . "," . (double) ($_GET['routewidth'] / 69) . ")"; $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL); if (PEAR::isError($this->sViewboxSmallSQL)) { failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL); } $this->sViewboxSmallSQL = "'" . $this->sViewboxSmallSQL . "'::geometry"; $sSQL = "select st_buffer(" . $sViewboxCentreSQL . "," . (double) ($_GET['routewidth'] / 30) . ")"; $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL); if (PEAR::isError($this->sViewboxLargeSQL)) { failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL); } $this->sViewboxLargeSQL = "'" . $this->sViewboxLargeSQL . "'::geometry"; $bBoundingBoxSearch = $this->bBoundedSearch; } // Do we have anything that looks like a lat/lon pair? if ($aLooksLike = looksLikeLatLonPair($sQuery)) { $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon'])); $sQuery = $aLooksLike['query']; } $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' => '', 'fLat' => '', 'fLon' => '', 'fRadius' => '')); // Do we have a radius search? $sNearPointSQL = false; if ($this->aNearPoint) { $sNearPointSQL = "ST_SetSRID(ST_Point(" . (double) $this->aNearPoint[1] . "," . (double) $this->aNearPoint[0] . "),4326)"; $aSearches[0]['fLat'] = (double) $this->aNearPoint[0]; $aSearches[0]['fLon'] = (double) $this->aNearPoint[1]; $aSearches[0]['fRadius'] = (double) $this->aNearPoint[2]; } // 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]; } 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']); unset($this->aStructuredQuery['amenity']); } foreach ($aSpecialTermsRaw as $aSpecialTerm) { $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery); $sToken = $this->oDB->getOne("select make_standard_name('" . $aSpecialTerm[1] . "') as string"); $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator'; $sSQL .= ' from word where word_token in (\' ' . $sToken . '\')) as x where (class is not null and class not in (\'place\')) or country_code is not null'; if (CONST_Debug) { var_Dump($sSQL); } $aSearchWords = $this->oDB->getAll($sSQL); $aNewSearches = array(); foreach ($aSearches as $aSearch) { 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; } } } $aSearches = $aNewSearches; } // Split query into phrases // Commas are used to reduce the search space by indicating where phrases split if ($this->aStructuredQuery) { $aPhrases = $this->aStructuredQuery; $bStructuredPhrases = true; } else { $aPhrases = explode(',', $sQuery); $bStructuredPhrases = false; } // Convert each phrase to standard form // Create a list of standard words // Get all 'sets' of words // Generate a complete list of all $aTokens = array(); foreach ($aPhrases as $iPhrase => $sPhrase) { $aPhrase = $this->oDB->getRow("select make_standard_name('" . pg_escape_string($sPhrase) . "') as string"); if (PEAR::isError($aPhrase)) { userError("Illegal query string (not an UTF-8 string): " . $sPhrase); if (CONST_Debug) { var_dump($aPhrase); } exit; } if (trim($aPhrase['string'])) { $aPhrases[$iPhrase] = $aPhrase; $aPhrases[$iPhrase]['words'] = explode(' ', $aPhrases[$iPhrase]['string']); $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0); $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets'])); } else { unset($aPhrases[$iPhrase]); } } // Reindex phrases - we make assumptions later on that they are numerically keyed in order $aPhraseTypes = array_keys($aPhrases); $aPhrases = array_values($aPhrases); if (sizeof($aTokens)) { // Check which tokens we have, get the ID numbers $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count'; $sSQL .= ' from word where word_token in (' . join(',', array_map("getDBQuoted", $aTokens)) . ')'; if (CONST_Debug) { var_Dump($sSQL); } $aValidTokens = array(); if (sizeof($aTokens)) { $aDatabaseWords = $this->oDB->getAll($sSQL); } else { $aDatabaseWords = array(); } if (PEAR::IsError($aDatabaseWords)) { failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords); } $aPossibleMainWordIDs = array(); $aWordFrequencyScores = array(); foreach ($aDatabaseWords as $aToken) { // Very special case - require 2 letter country param to match the country code found if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country']) && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code']) { continue; } if (isset($aValidTokens[$aToken['word_token']])) { $aValidTokens[$aToken['word_token']][] = $aToken; } else { $aValidTokens[$aToken['word_token']] = array($aToken); } if (!$aToken['class'] && !$aToken['country_code']) { $aPossibleMainWordIDs[$aToken['word_id']] = 1; } $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1; } if (CONST_Debug) { var_Dump($aPhrases, $aValidTokens); } // Try and calculate GB postcodes we might be missing 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; } } else { 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']) { if (isset($aValidTokens[$sToken])) { $aValidTokens[$sToken][] = $aToken; } else { $aValidTokens[$sToken] = array($aToken); } } } } } } } 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')); } } // Any words that have failed completely? // TODO: suggestions // Start the search process $aResultPlaceIDs = array(); $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases); if ($this->bReverseInPlan) { // Reverse phrase array and also reverse the order of the wordsets in // the first and final phrase. Don't bother about phrases in the middle // because order in the address doesn't matter. $aPhrases = array_reverse($aPhrases); $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0); if (sizeof($aPhrases) > 1) { $aFinalPhrase = end($aPhrases); $aPhrases[sizeof($aPhrases) - 1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0); } $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false); 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; } } } $aGroupedSearches = $aReverseGroupedSearches; ksort($aGroupedSearches); } } else { // 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; } } } ksort($aGroupedSearches); } // Filter out duplicate searches $aSearchHash = array(); foreach ($aGroupedSearches as $iGroup => $aSearches) { foreach ($aSearches as $iSearch => $aSearch) { $sHash = serialize($aSearch); if (isset($aSearchHash[$sHash])) { unset($aGroupedSearches[$iGroup][$iSearch]); if (sizeof($aGroupedSearches[$iGroup]) == 0) { unset($aGroupedSearches[$iGroup]); } } else { $aSearchHash[$sHash] = 1; } } } if (CONST_Debug) { _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens); } $iGroupLoop = 0; $iQueryLoop = 0; foreach ($aGroupedSearches as $iGroupedRank => $aSearches) { $iGroupLoop++; foreach ($aSearches as $aSearch) { $iQueryLoop++; if (CONST_Debug) { echo "<hr><b>Search Loop, group {$iGroupLoop}, loop {$iQueryLoop}</b>"; } if (CONST_Debug) { _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens); } // No location term? if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon']) { 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 = $this->oDB->getCol($sSQL); } else { $aPlaceIDs = array(); } } else { if (!$bBoundingBoxSearch && !$aSearch['fLon']) { continue; } if (!$aSearch['sClass']) { continue; } $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_" . $aSearch['sClass'] . "_" . $aSearch['sType'] . "'"; if ($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 ($sViewboxCentreSQL) { $sSQL .= " order by st_distance({$sViewboxCentreSQL}, ct.centroid) asc"; } $sSQL .= " limit {$this->iLimit}"; if (CONST_Debug) { var_dump($sSQL); } $aPlaceIDs = $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 ($sViewboxCentreSQL) { $sSQL .= " order by st_distance({$sViewboxCentreSQL}, ct.centroid) asc"; } $sSQL .= " limit {$this->iLimit}"; if (CONST_Debug) { var_dump($sSQL); } $aPlaceIDs = $this->oDB->getCol($sSQL); } } else { $sSQL = "select place_id from placex where class='" . $aSearch['sClass'] . "' and type='" . $aSearch['sType'] . "'"; $sSQL .= " and st_contains({$this->sViewboxSmallSQL}, geometry) and linked_place_id is null"; if ($sCountryCodesSQL) { $sSQL .= " and calculated_country_code in ({$sCountryCodesSQL})"; } if ($sViewboxCentreSQL) { $sSQL .= " order by st_distance({$sViewboxCentreSQL}, centroid) asc"; } $sSQL .= " limit {$this->iLimit}"; if (CONST_Debug) { var_dump($sSQL); } $aPlaceIDs = $this->oDB->getCol($sSQL); } } } 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[] = "exists(select place_id from placex where parent_place_id = search_name.place_id and transliteration(housenumber) ~* E'" . $sHouseNumberRegex . "' limit 1) 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['fLon'] && $aSearch['fLat']) { $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(" . $aSearch['fLon'] . "," . $aSearch['fLat'] . "),4326), " . $aSearch['fRadius'] . ")"; $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(" . $aSearch['fLon'] . "," . $aSearch['fLat'] . "),4326)) ASC"; } 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 ($sNearPointSQL) { $aOrder[] = "ST_Distance({$sNearPointSQL}, centroid) asc"; } 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 ($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 = '(select count(*) from (select unnest(ARRAY[' . join($aSearch['aFullNameAddress'], ",") . ']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch'; $aOrder[] = 'exactmatch DESC'; } else { $sExactMatchSQL = '0::int as exactmatch'; } 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 = $this->oDB->getAll($sSQL); if (PEAR::IsError($aViewBoxPlaceIDs)) { failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs); } //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']; } } //var_Dump($aPlaceIDs); //exit; if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs)) { $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 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 = $this->oDB->getCol($sSQL); // If not try the aux fallback table if (!sizeof($aPlaceIDs)) { $sSQL = "select place_id from location_property_aux where parent_place_id in (" . $sPlaceIDs . ") and housenumber = '" . pg_escape_string($aSearch['sHouseNumber']) . "'"; if (sizeof($this->aExcludePlaceIDs)) { $sSQL .= " and place_id not in (" . join(',', $this->aExcludePlaceIDs) . ")"; } //$sSQL .= " limit $this->iLimit"; if (CONST_Debug) { var_dump($sSQL); } $aPlaceIDs = $this->oDB->getCol($sSQL); } if (!sizeof($aPlaceIDs)) { $sSQL = "select place_id from location_property_tiger where parent_place_id in (" . $sPlaceIDs . ") and housenumber = '" . pg_escape_string($aSearch['sHouseNumber']) . "'"; if (sizeof($this->aExcludePlaceIDs)) { $sSQL .= " and place_id not in (" . join(',', $this->aExcludePlaceIDs) . ")"; } //$sSQL .= " limit $this->iLimit"; if (CONST_Debug) { var_dump($sSQL); } $aPlaceIDs = $this->oDB->getCol($sSQL); } // Fallback to the road if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber'])) { $aPlaceIDs = $aRoadPlaceIDs; } } 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 from placex where place_id in ({$sPlaceIDs}) and class='" . $aSearch['sClass'] . "' 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 limit {$this->iLimit}"; if (CONST_Debug) { var_dump($sSQL); } $aClassPlaceIDs = $this->oDB->getCol($sSQL); } if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') { $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_" . $aSearch['sClass'] . "_" . $aSearch['sType'] . "'"; $bCacheTable = $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) $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 from placex where place_id in ({$sPlaceIDs}) and rank_search < {$this->iMaxRank} + 5 and st_geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon') order by rank_search asc limit 1"; if (CONST_Debug) { var_dump($sSQL); } $sPlaceGeom = $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 = $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 ($sNearPointSQL) { $sOrderBySQL = "ST_Distance({$sNearPointSQL}, l.centroid)"; } else { if ($sPlaceIDs) { $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)"; } else { if ($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, $this->oDB->getCol($sSQL)); } else { if (isset($aSearch['fRadius']) && $aSearch['fRadius']) { $fRange = $aSearch['fRadius']; } $sOrderBySQL = ''; if ($sNearPointSQL) { $sOrderBySQL = "ST_Distance({$sNearPointSQL}, l.geometry)"; } else { $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)"; } $sSQL = "select distinct l.place_id" . ($sOrderBysSQL ? ',' . $sOrderBysSQL : '') . " from placex as l,placex as f where "; $sSQL .= "f.place_id in ( {$sPlaceIDs}) and ST_DWithin(l.geometry, f.centroid, {$fRange}) "; $sSQL .= "and l.class='" . $aSearch['sClass'] . "' 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, $this->oDB->getCol($sSQL)); } } } $aPlaceIDs = $aClassPlaceIDs; } } if (PEAR::IsError($aPlaceIDs)) { failInternalError("Could not get place IDs from tokens.", $sSQL, $aPlaceIDs); } if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); } foreach ($aPlaceIDs as $iPlaceID) { $aResultPlaceIDs[$iPlaceID] = $iPlaceID; } if ($iQueryLoop > 20) { break; } } if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30)) { // Need to verify passes rank limits before dropping out of the loop (yuk!) $sSQL = "select place_id from placex where place_id in (" . join(',', $aResultPlaceIDs) . ") "; $sSQL .= "and (placex.rank_address between {$this->iMinAddressRank} and {$this->iMaxAddressRank} "; if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) { $sSQL .= " OR (extratags->'place') = 'city'"; } if ($this->aAddressRankList) { $sSQL .= " OR placex.rank_address in (" . join(',', $this->aAddressRankList) . ")"; } $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (" . join(',', $aResultPlaceIDs) . ") "; $sSQL .= "and (30 between {$this->iMinAddressRank} and {$this->iMaxAddressRank} "; if ($this->aAddressRankList) { $sSQL .= " OR 30 in (" . join(',', $this->aAddressRankList) . ")"; } $sSQL .= ")"; if (CONST_Debug) { var_dump($sSQL); } $aResultPlaceIDs = $this->oDB->getCol($sSQL); } //exit; if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) { break; } if ($iGroupLoop > 4) { break; } if ($iQueryLoop > 30) { break; } } // Did we find anything? if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) { $aSearchResults = $this->getDetails($aResultPlaceIDs); } } else { // Just interpret as a reverse geocode $iPlaceID = geocodeReverse((double) $this->aNearPoint[0], (double) $this->aNearPoint[1]); if ($iPlaceID) { $aSearchResults = $this->getDetails(array($iPlaceID)); } else { $aSearchResults = array(); } } // No results? Done if (!sizeof($aSearchResults)) { if ($this->bFallback) { if ($this->fallbackStructuredQuery()) { return $this->lookup(); } } return array(); } $aClassType = getClassTypesWithImportance(); $aRecheckWords = preg_split('/\\b[\\s,\\-]*/u', $sQuery); foreach ($aRecheckWords as $i => $sWord) { if (!preg_match('/\\pL/', $sWord)) { unset($aRecheckWords[$i]); } } if (CONST_Debug) { echo '<i>Recheck words:<\\i>'; var_dump($aRecheckWords); } foreach ($aSearchResults as $iResNum => $aResult) { // Default $fDiameter = 0.0001; if (isset($aClassType[$aResult['class'] . ':' . $aResult['type'] . ':' . $aResult['admin_level']]['defdiameter']) && $aClassType[$aResult['class'] . ':' . $aResult['type'] . ':' . $aResult['admin_level']]['defdiameter']) { $fDiameter = $aClassType[$aResult['class'] . ':' . $aResult['type'] . ':' . $aResult['admin_level']]['defdiameter']; } elseif (isset($aClassType[$aResult['class'] . ':' . $aResult['type']]['defdiameter']) && $aClassType[$aResult['class'] . ':' . $aResult['type']]['defdiameter']) { $fDiameter = $aClassType[$aResult['class'] . ':' . $aResult['type']]['defdiameter']; } $fRadius = $fDiameter / 2; if (CONST_Search_AreaPolygons) { // Get the bounding box and outline polygon $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,"; $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,"; $sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,"; $sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon"; if ($this->bIncludePolygonAsGeoJSON) { $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson"; } if ($this->bIncludePolygonAsKML) { $sSQL .= ",ST_AsKML(geometry) as askml"; } if ($this->bIncludePolygonAsSVG) { $sSQL .= ",ST_AsSVG(geometry) as assvg"; } if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) { $sSQL .= ",ST_AsText(geometry) as astext"; } $sFrom = " from placex where place_id = " . $aResult['place_id']; if ($this->fPolygonSimplificationThreshold > 0) { $sSQL .= " from (select place_id,centroid,ST_SimplifyPreserveTopology(geometry," . $this->fPolygonSimplificationThreshold . ") as geometry" . $sFrom . ") as plx"; } else { $sSQL .= $sFrom; } $aPointPolygon = $this->oDB->getRow($sSQL); if (PEAR::IsError($aPointPolygon)) { failInternalError("Could not get outline.", $sSQL, $aPointPolygon); } if ($aPointPolygon['place_id']) { if ($this->bIncludePolygonAsGeoJSON) { $aResult['asgeojson'] = $aPointPolygon['asgeojson']; } if ($this->bIncludePolygonAsKML) { $aResult['askml'] = $aPointPolygon['askml']; } if ($this->bIncludePolygonAsSVG) { $aResult['assvg'] = $aPointPolygon['assvg']; } if ($this->bIncludePolygonAsText) { $aResult['astext'] = $aPointPolygon['astext']; } if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null) { $aResult['lat'] = $aPointPolygon['centrelat']; $aResult['lon'] = $aPointPolygon['centrelon']; } if ($this->bIncludePolygonAsPoints) { // Translate geometry string to point array if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#', $aPointPolygon['astext'], $aMatch)) { preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER); } elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#', $aPointPolygon['astext'], $aMatch)) { preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER); } elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#', $aPointPolygon['astext'], $aMatch)) { $iSteps = max(8, min(100, $fRadius * 40000 ^ 2)); $fStepSize = 2 * pi() / $iSteps; $aPolyPoints = array(); for ($f = 0; $f < 2 * pi(); $f += $fStepSize) { $aPolyPoints[] = array('', $aMatch[1] + $fRadius * sin($f), $aMatch[2] + $fRadius * cos($f)); } } } // Output data suitable for display (points and a bounding box) if ($this->bIncludePolygonAsPoints && isset($aPolyPoints)) { $aResult['aPolyPoints'] = array(); foreach ($aPolyPoints as $aPoint) { $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]); } } if (abs($aPointPolygon['minlat'] - $aPointPolygon['maxlat']) < 1.0E-7) { $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius; $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius; } if (abs($aPointPolygon['minlon'] - $aPointPolygon['maxlon']) < 1.0E-7) { $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius; $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius; } $aResult['aBoundingBox'] = array((string) $aPointPolygon['minlat'], (string) $aPointPolygon['maxlat'], (string) $aPointPolygon['minlon'], (string) $aPointPolygon['maxlon']); } } if ($aResult['extra_place'] == 'city') { $aResult['class'] = 'place'; $aResult['type'] = 'city'; $aResult['rank_search'] = 16; } if (!isset($aResult['aBoundingBox'])) { $iSteps = max(8, min(100, $fRadius * 3.14 * 100000)); $fStepSize = 2 * pi() / $iSteps; $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius; $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius; $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius; $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius; // Output data suitable for display (points and a bounding box) if ($this->bIncludePolygonAsPoints) { $aPolyPoints = array(); for ($f = 0; $f < 2 * pi(); $f += $fStepSize) { $aPolyPoints[] = array('', $aResult['lon'] + $fRadius * sin($f), $aResult['lat'] + $fRadius * cos($f)); } $aResult['aPolyPoints'] = array(); foreach ($aPolyPoints as $aPoint) { $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]); } } $aResult['aBoundingBox'] = array((string) $aPointPolygon['minlat'], (string) $aPointPolygon['maxlat'], (string) $aPointPolygon['minlon'], (string) $aPointPolygon['maxlon']); } // Is there an icon set for this type of result? if (isset($aClassType[$aResult['class'] . ':' . $aResult['type']]['icon']) && $aClassType[$aResult['class'] . ':' . $aResult['type']]['icon']) { $aResult['icon'] = CONST_Website_BaseURL . 'images/mapicons/' . $aClassType[$aResult['class'] . ':' . $aResult['type']]['icon'] . '.p.20.png'; } if (isset($aClassType[$aResult['class'] . ':' . $aResult['type'] . ':' . $aResult['admin_level']]['label']) && $aClassType[$aResult['class'] . ':' . $aResult['type'] . ':' . $aResult['admin_level']]['label']) { $aResult['label'] = $aClassType[$aResult['class'] . ':' . $aResult['type'] . ':' . $aResult['admin_level']]['label']; } elseif (isset($aClassType[$aResult['class'] . ':' . $aResult['type']]['label']) && $aClassType[$aResult['class'] . ':' . $aResult['type']]['label']) { $aResult['label'] = $aClassType[$aResult['class'] . ':' . $aResult['type']]['label']; } if ($this->bIncludeAddressDetails) { $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']); if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city'])) { $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']); } } if ($this->bIncludeExtraTags) { if ($aResult['extra']) { $aResult['sExtraTags'] = json_decode($aResult['extra']); } else { $aResult['sExtraTags'] = (object) array(); } } if ($this->bIncludeNameDetails) { if ($aResult['names']) { $aResult['sNameDetails'] = json_decode($aResult['names']); } else { $aResult['sNameDetails'] = (object) array(); } } // Adjust importance for the number of exact string matches in the result $aResult['importance'] = max(0.001, $aResult['importance']); $iCountWords = 0; $sAddress = $aResult['langaddress']; foreach ($aRecheckWords as $i => $sWord) { if (stripos($sAddress, $sWord) !== false) { $iCountWords++; if (preg_match("/(^|,)\\s*" . preg_quote($sWord, '/') . "\\s*(,|\$)/", $sAddress)) { $iCountWords += 0.1; } } } $aResult['importance'] = $aResult['importance'] + $iCountWords * 0.1; // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right $aResult['name'] = $aResult['langaddress']; // secondary ordering (for results with same importance (the smaller the better): // - approximate importance of address parts $aResult['foundorder'] = -$aResult['addressimportance'] / 10; // - number of exact matches from the query if (isset($this->exactMatchCache[$aResult['place_id']])) { $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']]; } else { if (isset($this->exactMatchCache[$aResult['parent_place_id']])) { $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']]; } } // - importance of the class/type if (isset($aClassType[$aResult['class'] . ':' . $aResult['type']]['importance']) && $aClassType[$aResult['class'] . ':' . $aResult['type']]['importance']) { $aResult['foundorder'] += 0.0001 * $aClassType[$aResult['class'] . ':' . $aResult['type']]['importance']; } else { $aResult['foundorder'] += 0.01; } if (CONST_Debug) { var_dump($aResult); } $aSearchResults[$iResNum] = $aResult; } uasort($aSearchResults, 'byImportance'); $aOSMIDDone = array(); $aClassTypeNameDone = array(); $aToFilter = $aSearchResults; $aSearchResults = array(); $bFirst = true; foreach ($aToFilter as $iResNum => $aResult) { $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id']; if ($bFirst) { $fLat = $aResult['lat']; $fLon = $aResult['lon']; if (isset($aResult['zoom'])) { $iZoom = $aResult['zoom']; } $bFirst = false; } if (!$this->bDeDupe || !isset($aOSMIDDone[$aResult['osm_type'] . $aResult['osm_id']]) && !isset($aClassTypeNameDone[$aResult['osm_type'] . $aResult['class'] . $aResult['type'] . $aResult['name'] . $aResult['admin_level']])) { $aOSMIDDone[$aResult['osm_type'] . $aResult['osm_id']] = true; $aClassTypeNameDone[$aResult['osm_type'] . $aResult['class'] . $aResult['type'] . $aResult['name'] . $aResult['admin_level']] = true; $aSearchResults[] = $aResult; } // Absolute limit on number of results if (sizeof($aSearchResults) >= $this->iFinalLimit) { break; } } return $aSearchResults; }
$hLog = logStart($oDB, 'details', $_SERVER['QUERY_STRING'], $aLangPrefOrder); // Make sure the point we are reporting on is fully indexed //$sSQL = "UPDATE placex set indexed = true where indexed = false and place_id = $iPlaceID"; //$oDB->query($sSQL); // Get the details for this point $sSQL = "select place_id, osm_type, osm_id, class, type, name, admin_level, housenumber, street, isin, postcode, calculated_country_code as country_code, importance, wikipedia,"; $sSQL .= " to_char(indexed_date, 'YYYY-MM-DD HH24:MI') as indexed_date, parent_place_id, rank_address, rank_search, get_searchrank_label(rank_search) as rank_search_label, get_name_by_language(name,{$sLanguagePrefArraySQL}) as localname, "; $sSQL .= " ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon') as isarea, "; //$sSQL .= " ST_Area(geometry::geography) as area, "; $sSQL .= " ST_y(centroid) as lat, ST_x(centroid) as lon,"; $sSQL .= " case when importance = 0 OR importance IS NULL then 0.75-(rank_search::float/40) else importance end as calculated_importance, "; $sSQL .= " ST_AsText(CASE WHEN ST_NPoints(geometry) > 5000 THEN ST_SimplifyPreserveTopology(geometry, 0.0001) ELSE geometry END) as outlinestring"; $sSQL .= " from placex where place_id = {$iPlaceID}"; $aPointDetails = $oDB->getRow($sSQL); if (PEAR::IsError($aPointDetails)) { failInternalError("Could not get details of place object.", $sSQL, $aPointDetails); } $aPointDetails['localname'] = $aPointDetails['localname'] ? $aPointDetails['localname'] : $aPointDetails['housenumber']; $aClassType = getClassTypesWithImportance(); $aPointDetails['icon'] = $aClassType[$aPointDetails['class'] . ':' . $aPointDetails['type']]['icon']; // Get all alternative names (languages, etc) $sSQL = "select (each(name)).key,(each(name)).value from placex where place_id = {$iPlaceID} order by (each(name)).key"; $aPointDetails['aNames'] = $oDB->getAssoc($sSQL); // Extra tags $sSQL = "select (each(extratags)).key,(each(extratags)).value from placex where place_id = {$iPlaceID} order by (each(extratags)).key"; $aPointDetails['aExtraTags'] = $oDB->getAssoc($sSQL); // Address $aAddressLines = getAddressDetails($oDB, $sLanguagePrefArraySQL, $iPlaceID, $aPointDetails['country_code'], true); // Linked places $sSQL = "select placex.place_id, osm_type, osm_id, class, type, housenumber, admin_level, rank_address, ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon') as isarea, st_distance(geometry, placegeometry) as distance, "; $sSQL .= " get_name_by_language(name,{$sLanguagePrefArraySQL}) as localname, length(name::text) as namelength ";
$aPlace = $oDB->getRow($sSQL); $iPlaceID = $aPlace['place_id']; $iParentPlaceID = $aPlace['parent_place_id']; if (PEAR::IsError($iPlaceID)) { failInternalError("Could not determine closest place.", $sSQL, $iPlaceID); } } // The point we found might be too small - use the address to find what it is a child of if ($iPlaceID && $iMaxRank < 28) { if ($aPlace['rank_search'] > 28 && $iParentPlaceID) { $iPlaceID = $iParentPlaceID; } $sSQL = "select address_place_id from place_addressline where place_id = {$iPlaceID} order by abs(cached_rank_address - {$iMaxRank}) asc,cached_rank_address desc,isaddress desc,distance desc limit 1"; $iPlaceID = $oDB->getOne($sSQL); if (PEAR::IsError($iPlaceID)) { failInternalError("Could not get parent for place.", $sSQL, $iPlaceID); } if (!$iPlaceID) { $iPlaceID = $aPlace['place_id']; } } } if ($iPlaceID) { $sSQL = "select placex.*,"; $sSQL .= " get_address_by_language(place_id, {$sLanguagePrefArraySQL}) as langaddress,"; $sSQL .= " get_name_by_language(name, {$sLanguagePrefArraySQL}) as placename,"; $sSQL .= " get_name_by_language(name, ARRAY['ref']) as ref,"; $sSQL .= " st_y(st_centroid(geometry)) as lat, st_x(st_centroid(geometry)) as lon"; $sSQL .= " from placex where place_id = {$iPlaceID} "; //var_dump($sSQL); $aPlace = $oDB->getRow($sSQL);
$aClassType = getClassTypesWithImportance(); $aPointDetails['icon'] = $aClassType[$aPointDetails['class'] . ':' . $aPointDetails['type']]['icon']; // Get all alternative names (languages, etc) $sSQL = "select (each(name)).key,(each(name)).value from placex where place_id = {$iPlaceID} order by (each(name)).key"; $aPointDetails['aNames'] = $oDB->getAssoc($sSQL); // Extra tags $sSQL = "select (each(extratags)).key,(each(extratags)).value from placex where place_id = {$iPlaceID} order by (each(extratags)).key"; $aPointDetails['aExtraTags'] = $oDB->getAssoc($sSQL); // Get the bounding box and outline polygon $sSQL = "select ST_AsText(geometry) as outlinestring,"; $sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,"; $sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon"; $sSQL .= " from placex where place_id = {$iPlaceID}"; $aPointPolygon = $oDB->getRow($sSQL); if (PEAR::IsError($aPointPolygon)) { failInternalError("Could not get bounding box of place object.", $sSQL, $aPointPolygon); } if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#', $aPointPolygon['outlinestring'], $aMatch)) { preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER); } elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#', $aPointPolygon['outlinestring'], $aMatch)) { // TODO: this just takes the first ring preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER); } elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#', $aPointPolygon['outlinestring'], $aMatch)) { $fRadius = 0.01; if ($aPointDetails['rank_search'] > 20) { $fRadius = 0.0001; } $iSteps = min(max($fRadius * 40000 ^ 2, 16), 100); $fStepSize = 2 * pi() / $iSteps; $aPolyPoints = array(); for ($f = 0; $f < 2 * pi(); $f += $fStepSize) {
} foreach ($aSearchResults as $iResNum => $aResult) { if (CONST_Search_AreaPolygons) { // Get the bounding box and outline polygon $sSQL = "select place_id,numfeatures,area,outline,"; $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(outline)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(outline)),2)) as maxlat,"; $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(outline)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(outline)),3)) as maxlon,"; $sSQL .= "ST_AsText(outline) as outlinestring from get_place_boundingbox_quick(" . $aResult['place_id'] . ")"; $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,"; $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,"; $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,"; $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon,"; $sSQL .= "ST_AsText(geometry) as outlinestring from placex where place_id = " . $aResult['place_id'] . ' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\''; $aPointPolygon = $oDB->getRow($sSQL); if (PEAR::IsError($aPointPolygon)) { failInternalError("Could not get outline.", $sSQL, $aPointPolygon); } if ($aPointPolygon['place_id']) { if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null) { $aResult['lat'] = $aPointPolygon['centrelat']; $aResult['lon'] = $aPointPolygon['centrelon']; } // Translate geometary string to point array if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#', $aPointPolygon['outlinestring'], $aMatch)) { preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER); } elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#', $aPointPolygon['outlinestring'], $aMatch)) { $fRadius = 0.01; $iSteps = $fRadius * 40000 ^ 2; $fStepSize = 2 * pi() / $iSteps; $aPolyPoints = array(); for ($f = 0; $f < 2 * pi(); $f += $fStepSize) {
<?php require_once dirname(dirname(__FILE__)) . '/lib/init-website.php'; require_once CONST_BasePath . '/lib/log.php'; $sOutputFormat = 'html'; ini_set('memory_limit', '200M'); $oDB =& getDB(); $sSQL = "select placex.place_id, calculated_country_code as country_code, name->'name' as name, i.* from placex, import_polygon_delete i where placex.osm_id = i.osm_id and placex.osm_type = i.osm_type and placex.class = i.class and placex.type = i.type"; $aPolygons = $oDB->getAll($sSQL); if (PEAR::isError($aPolygons)) { failInternalError("Could not get list of deleted OSM elements.", $sSQL, $aPolygons); } //var_dump($aPolygons); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" > <title>Nominatim Deleted Data</title> <meta name="description" content="List of OSM data that has been deleted" lang="en-US" /> </head> <body> <style type="text/css"> table { border-width: 1px; border-spacing: 0px;
function getOutlines($iPlaceID, $fLon = null, $fLat = null, $fRadius = null) { $aOutlineResult = array(); if (!$iPlaceID) { return $aOutlineResult; } if (CONST_Search_AreaPolygons) { // Get the bounding box and outline polygon $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,"; $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,"; $sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,"; $sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon"; if ($this->bIncludePolygonAsGeoJSON) { $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson"; } if ($this->bIncludePolygonAsKML) { $sSQL .= ",ST_AsKML(geometry) as askml"; } if ($this->bIncludePolygonAsSVG) { $sSQL .= ",ST_AsSVG(geometry) as assvg"; } if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) { $sSQL .= ",ST_AsText(geometry) as astext"; } $sFrom = " from placex where place_id = " . $iPlaceID; if ($this->fPolygonSimplificationThreshold > 0) { $sSQL .= " from (select place_id,centroid,ST_SimplifyPreserveTopology(geometry," . $this->fPolygonSimplificationThreshold . ") as geometry" . $sFrom . ") as plx"; } else { $sSQL .= $sFrom; } $aPointPolygon = $this->oDB->getRow($sSQL); if (PEAR::IsError($aPointPolygon)) { echo var_dump($aPointPolygon); failInternalError("Could not get outline.", $sSQL, $aPointPolygon); } if ($aPointPolygon['place_id']) { if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null) { $aOutlineResult['lat'] = $aPointPolygon['centrelat']; $aOutlineResult['lon'] = $aPointPolygon['centrelon']; } if ($this->bIncludePolygonAsGeoJSON) { $aOutlineResult['asgeojson'] = $aPointPolygon['asgeojson']; } if ($this->bIncludePolygonAsKML) { $aOutlineResult['askml'] = $aPointPolygon['askml']; } if ($this->bIncludePolygonAsSVG) { $aOutlineResult['assvg'] = $aPointPolygon['assvg']; } if ($this->bIncludePolygonAsText) { $aOutlineResult['astext'] = $aPointPolygon['astext']; } if ($this->bIncludePolygonAsPoints) { $aOutlineResult['aPolyPoints'] = geometryText2Points($aPointPolygon['astext'], $fRadius); } if (abs($aPointPolygon['minlat'] - $aPointPolygon['maxlat']) < 1.0E-7) { $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius; $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius; } if (abs($aPointPolygon['minlon'] - $aPointPolygon['maxlon']) < 1.0E-7) { $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius; $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius; } $aOutlineResult['aBoundingBox'] = array((string) $aPointPolygon['minlat'], (string) $aPointPolygon['maxlat'], (string) $aPointPolygon['minlon'], (string) $aPointPolygon['maxlon']); } } // CONST_Search_AreaPolygons // as a fallback we generate a bounding box without knowing the size of the geometry if (!isset($aOutlineResult['aBoundingBox']) && isset($fLon)) { if ($this->bIncludePolygonAsPoints) { $sGeometryText = 'POINT(' . $fLon . ',' . $fLat . ')'; $aOutlineResult['aPolyPoints'] = geometryText2Points($sGeometryText, $fRadius); } $aBounds = array(); $aBounds['minlat'] = $fLat - $fRadius; $aBounds['maxlat'] = $fLat + $fRadius; $aBounds['minlon'] = $fLon - $fRadius; $aBounds['maxlon'] = $fLon + $fRadius; $aOutlineResult['aBoundingBox'] = array((string) $aBounds['minlat'], (string) $aBounds['maxlat'], (string) $aBounds['minlon'], (string) $aBounds['maxlon']); } return $aOutlineResult; }
function lookup() { $sPointSQL = 'ST_SetSRID(ST_Point(' . $this->fLon . ',' . $this->fLat . '),4326)'; $iMaxRank = $this->iMaxRank; $iMaxRank_orig = $this->iMaxRank; // Find the nearest point $fSearchDiam = 0.0004; $iPlaceID = null; $aArea = false; $fMaxAreaDistance = 1; $bIsInUnitedStates = false; $bPlaceIsTiger = false; while (!$iPlaceID && $fSearchDiam < $fMaxAreaDistance) { $fSearchDiam = $fSearchDiam * 2; // If we have to expand the search area by a large amount then we need a larger feature // then there is a limit to how small the feature should be if ($fSearchDiam > 2 && $iMaxRank > 4) { $iMaxRank = 4; } if ($fSearchDiam > 1 && $iMaxRank > 9) { $iMaxRank = 8; } if ($fSearchDiam > 0.8 && $iMaxRank > 10) { $iMaxRank = 10; } if ($fSearchDiam > 0.6 && $iMaxRank > 12) { $iMaxRank = 12; } if ($fSearchDiam > 0.2 && $iMaxRank > 17) { $iMaxRank = 17; } if ($fSearchDiam > 0.1 && $iMaxRank > 18) { $iMaxRank = 18; } if ($fSearchDiam > 0.008 && $iMaxRank > 22) { $iMaxRank = 22; } if ($fSearchDiam > 0.001 && $iMaxRank > 26) { $iMaxRank = 26; } $sSQL = 'select place_id,parent_place_id,rank_search,calculated_country_code from placex'; $sSQL .= ' WHERE ST_DWithin(' . $sPointSQL . ', geometry, ' . $fSearchDiam . ')'; $sSQL .= ' and rank_search != 28 and rank_search >= ' . $iMaxRank; $sSQL .= ' and (name is not null or housenumber is not null)'; $sSQL .= ' and class not in (\'waterway\',\'railway\',\'tunnel\',\'bridge\',\'man_made\')'; $sSQL .= ' and indexed_status = 0 '; $sSQL .= ' and (ST_GeometryType(geometry) not in (\'ST_Polygon\',\'ST_MultiPolygon\') '; $sSQL .= ' OR ST_DWithin(' . $sPointSQL . ', centroid, ' . $fSearchDiam . '))'; $sSQL .= ' ORDER BY ST_distance(' . $sPointSQL . ', geometry) ASC limit 1'; if (CONST_Debug) { var_dump($sSQL); } $aPlace = $this->oDB->getRow($sSQL); if (PEAR::IsError($aPlace)) { failInternalError("Could not determine closest place.", $sSQL, $aPlace); } $iPlaceID = $aPlace['place_id']; $iParentPlaceID = $aPlace['parent_place_id']; $bIsInUnitedStates = $aPlace['calculated_country_code'] == 'us'; } // Only street found? If it's in the US we can check TIGER data for nearest housenumber if ($bIsInUnitedStates && $iMaxRank_orig >= 28 && $iPlaceID && ($aPlace['rank_search'] == 26 || $aPlace['rank_search'] == 27)) { $fSearchDiam = 0.001; $sSQL = 'SELECT place_id,parent_place_id,30 as rank_search '; if (CONST_Debug) { $sSQL .= ', housenumber, ST_distance(' . $sPointSQL . ', centroid) as distance, st_y(centroid) as lat, st_x(centroid) as lon'; } $sSQL .= ' FROM location_property_tiger WHERE parent_place_id = ' . $iPlaceID; $sSQL .= ' AND ST_DWithin(' . $sPointSQL . ', centroid, ' . $fSearchDiam . ')'; $sSQL .= ' ORDER BY ST_distance(' . $sPointSQL . ', centroid) ASC limit 1'; // print all house numbers in the parent (street) if (CONST_Debug) { $sSQL = preg_replace('/limit 1/', 'limit 100', $sSQL); var_dump($sSQL); $aAllHouses = $this->oDB->getAll($sSQL); foreach ($aAllHouses as $i) { echo $i['housenumber'] . ' | ' . $i['distance'] * 1000 . ' | ' . $i['lat'] . ' | ' . $i['lon'] . ' | ' . "<br>\n"; } } $aPlaceTiger = $this->oDB->getRow($sSQL); if (PEAR::IsError($aPlace)) { failInternalError("Could not determine closest Tiger place.", $sSQL, $aPlaceTiger); } if ($aPlaceTiger) { if (CONST_Debug) { var_dump('found Tiger place', $aPlaceTiger); } $bPlaceIsTiger = true; $aPlace = $aPlaceTiger; $iPlaceID = $aPlaceTiger['place_id']; $iParentPlaceID = $aPlaceTiger['parent_place_id']; // the street } } // The point we found might be too small - use the address to find what it is a child of if ($iPlaceID && $iMaxRank < 28) { if ($aPlace['rank_search'] > 28 && $iParentPlaceID && !$bPlaceIsTiger) { $iPlaceID = $iParentPlaceID; } $sSQL = "select address_place_id from place_addressline where place_id = {$iPlaceID} order by abs(cached_rank_address - {$iMaxRank}) asc,cached_rank_address desc,isaddress desc,distance desc limit 1"; $iPlaceID = $this->oDB->getOne($sSQL); if (PEAR::IsError($iPlaceID)) { failInternalError("Could not get parent for place.", $sSQL, $iPlaceID); } if (!$iPlaceID) { $iPlaceID = $aPlace['place_id']; } } return array('place_id' => $iPlaceID, 'type' => $bPlaceIsTiger ? 'tiger' : 'osm'); }