public function test_getWordSets() { // given an array of arrays like // array( array('a','b'), array('c','d') ) // returns a summary as string: '(a|b),(c|d)' function serialize_sets($aSets) { $aParts = array(); foreach ($aSets as $aSet) { $aParts[] = '(' . join('|', $aSet) . ')'; } return join(',', $aParts); } $this->assertEquals(array(array('')), getWordSets(array(), 0)); $this->assertEquals('(a)', serialize_sets(getWordSets(array("a"), 0))); $this->assertEquals('(a b),(a|b)', serialize_sets(getWordSets(array('a', 'b'), 0))); $this->assertEquals('(a b c),(a|b c),(a|b|c),(a b|c)', serialize_sets(getWordSets(array('a', 'b', 'c'), 0))); $this->assertEquals('(a b c d),(a|b c d),(a|b|c d),(a|b|c|d),(a|b c|d),(a b|c d),(a b|c|d),(a b c|d)', serialize_sets(getWordSets(array('a', 'b', 'c', 'd'), 0))); // Inverse $this->assertEquals('(a b c),(c|a b),(c|b|a),(b c|a)', serialize_sets(getInverseWordSets(array('a', 'b', 'c'), 0))); // make sure we don't create too many sets // 4 words => 8 sets // 10 words => 511 sets // 15 words => 12911 sets // 18 words => 65536 sets // 20 words => 169766 sets // 22 words => 401930 sets // 28 words => 3505699 sets (needs more than 4GB via 'phpunit -d memory_limit=' to run) $this->assertEquals(8, count(getWordSets(array_fill(0, 4, 'a'), 0))); $this->assertEquals(65536, count(getWordSets(array_fill(0, 18, 'a'), 0))); }
function getWordSets($aWords) { $aResult = array(array(join(' ', $aWords))); $sFirstToken = ''; while (sizeof($aWords) > 1) { $sWord = array_shift($aWords); $sFirstToken .= ($sFirstToken ? ' ' : '') . $sWord; $aRest = getWordSets($aWords); foreach ($aRest as $aSet) { $aResult[] = array_merge(array($sFirstToken), $aSet); } } return $aResult; }
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; }
// Get all 'sets' of words // Generate a complete list of all $aTokens = array(); foreach ($aPhrases as $iPhrase => $sPhrase) { $aPhrase = $oDB->getRow("select make_standard_name('" . pg_escape_string($sPhrase) . "') as string"); if (PEAR::isError($aPhrase)) { echo "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']); $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets'])); } else { unset($aPhrases[$iPhrase]); } } // reindex phrases - we make assumptions later on $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, location, country_code, operator'; $sSQL .= ' from word where word_token in (' . join(',', array_map("getDBQuoted", $aTokens)) . ')'; $sSQL .= ' and (class is null or class not in (\'highway\'))'; // $sSQL .= ' group by word_token, word, class, type, location, country_code'; if (CONST_Debug) { var_Dump($sSQL);