2 require_once(CONST_BasePath.'/lib/PlaceLookup.php');
8 protected $aLangPrefOrder = array();
10 protected $bIncludeAddressDetails = false;
11 protected $bIncludeExtraTags = false;
12 protected $bIncludeNameDetails = false;
14 protected $bIncludePolygonAsPoints = false;
15 protected $bIncludePolygonAsText = false;
16 protected $bIncludePolygonAsGeoJSON = false;
17 protected $bIncludePolygonAsKML = false;
18 protected $bIncludePolygonAsSVG = false;
19 protected $fPolygonSimplificationThreshold = 0.0;
21 protected $aExcludePlaceIDs = array();
22 protected $bDeDupe = true;
23 protected $bReverseInPlan = true;
25 protected $iLimit = 20;
26 protected $iFinalLimit = 10;
27 protected $iOffset = 0;
28 protected $bFallback = false;
30 protected $aCountryCodes = false;
31 protected $aNearPoint = false;
33 protected $bBoundedSearch = false;
34 protected $aViewBox = false;
35 protected $sViewboxSmallSQL = false;
36 protected $sViewboxLargeSQL = false;
37 protected $aRoutePoints = false;
39 protected $iMaxRank = 20;
40 protected $iMinAddressRank = 0;
41 protected $iMaxAddressRank = 30;
42 protected $aAddressRankList = array();
43 protected $exactMatchCache = array();
45 protected $sAllowedTypesSQLList = false;
47 protected $sQuery = false;
48 protected $aStructuredQuery = false;
50 function Geocode(&$oDB)
55 function setReverseInPlan($bReverse)
57 $this->bReverseInPlan = $bReverse;
60 function setLanguagePreference($aLangPref)
62 $this->aLangPrefOrder = $aLangPref;
65 function setIncludeAddressDetails($bAddressDetails = true)
67 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
70 function getIncludeAddressDetails()
72 return $this->bIncludeAddressDetails;
75 function getIncludeExtraTags()
77 return $this->bIncludeExtraTags;
80 function getIncludeNameDetails()
82 return $this->bIncludeNameDetails;
85 function setIncludePolygonAsPoints($b = true)
87 $this->bIncludePolygonAsPoints = $b;
90 function getIncludePolygonAsPoints()
92 return $this->bIncludePolygonAsPoints;
95 function setIncludePolygonAsText($b = true)
97 $this->bIncludePolygonAsText = $b;
100 function getIncludePolygonAsText()
102 return $this->bIncludePolygonAsText;
105 function setIncludePolygonAsGeoJSON($b = true)
107 $this->bIncludePolygonAsGeoJSON = $b;
110 function setIncludePolygonAsKML($b = true)
112 $this->bIncludePolygonAsKML = $b;
115 function setIncludePolygonAsSVG($b = true)
117 $this->bIncludePolygonAsSVG = $b;
120 function setPolygonSimplificationThreshold($f)
122 $this->fPolygonSimplificationThreshold = $f;
125 function setDeDupe($bDeDupe = true)
127 $this->bDeDupe = (bool)$bDeDupe;
130 function setLimit($iLimit = 10)
132 if ($iLimit > 50) $iLimit = 50;
133 if ($iLimit < 1) $iLimit = 1;
135 $this->iFinalLimit = $iLimit;
136 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
139 function setOffset($iOffset = 0)
141 $this->iOffset = $iOffset;
144 function setFallback($bFallback = true)
146 $this->bFallback = (bool)$bFallback;
149 function setExcludedPlaceIDs($a)
151 // TODO: force to int
152 $this->aExcludePlaceIDs = $a;
155 function getExcludedPlaceIDs()
157 return $this->aExcludePlaceIDs;
160 function setBounded($bBoundedSearch = true)
162 $this->bBoundedSearch = (bool)$bBoundedSearch;
165 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
167 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
170 function getViewBoxString()
172 if (!$this->aViewBox) return null;
173 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
176 function setRoute($aRoutePoints)
178 $this->aRoutePoints = $aRoutePoints;
181 function setFeatureType($sFeatureType)
183 switch($sFeatureType)
186 $this->setRankRange(4, 4);
189 $this->setRankRange(8, 8);
192 $this->setRankRange(14, 16);
195 $this->setRankRange(8, 20);
200 function setRankRange($iMin, $iMax)
202 $this->iMinAddressRank = (int)$iMin;
203 $this->iMaxAddressRank = (int)$iMax;
206 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
208 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
211 function setCountryCodesList($aCountryCodes)
213 $this->aCountryCodes = $aCountryCodes;
216 function setQuery($sQueryString)
218 $this->sQuery = $sQueryString;
219 $this->aStructuredQuery = false;
222 function getQueryString()
224 return $this->sQuery;
228 function loadParamArray($aParams)
230 if (isset($aParams['addressdetails'])) $this->bIncludeAddressDetails = (bool)$aParams['addressdetails'];
231 if ((float) CONST_Postgresql_Version > 9.2)
233 if (isset($aParams['extratags'])) $this->bIncludeExtraTags = (bool)$aParams['extratags'];
234 if (isset($aParams['namedetails'])) $this->bIncludeNameDetails = (bool)$aParams['namedetails'];
236 if (isset($aParams['bounded'])) $this->bBoundedSearch = (bool)$aParams['bounded'];
237 if (isset($aParams['dedupe'])) $this->bDeDupe = (bool)$aParams['dedupe'];
239 if (isset($aParams['limit'])) $this->setLimit((int)$aParams['limit']);
240 if (isset($aParams['offset'])) $this->iOffset = (int)$aParams['offset'];
242 if (isset($aParams['fallback'])) $this->bFallback = (bool)$aParams['fallback'];
244 // List of excluded Place IDs - used for more acurate pageing
245 if (isset($aParams['exclude_place_ids']) && $aParams['exclude_place_ids'])
247 foreach(explode(',',$aParams['exclude_place_ids']) as $iExcludedPlaceID)
249 $iExcludedPlaceID = (int)$iExcludedPlaceID;
250 if ($iExcludedPlaceID)
251 $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
254 if (isset($aExcludePlaceIDs))
255 $this->aExcludePlaceIDs = $aExcludePlaceIDs;
258 // Only certain ranks of feature
259 if (isset($aParams['featureType'])) $this->setFeatureType($aParams['featureType']);
260 if (isset($aParams['featuretype'])) $this->setFeatureType($aParams['featuretype']);
263 if (isset($aParams['countrycodes']))
265 $aCountryCodes = array();
266 foreach(explode(',',$aParams['countrycodes']) as $sCountryCode)
268 if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
270 $aCountryCodes[] = strtolower($sCountryCode);
273 $this->aCountryCodes = $aCountryCodes;
276 if (isset($aParams['viewboxlbrt']) && $aParams['viewboxlbrt'])
278 $aCoOrdinatesLBRT = explode(',',$aParams['viewboxlbrt']);
279 $this->setViewBox($aCoOrdinatesLBRT[0], $aCoOrdinatesLBRT[1], $aCoOrdinatesLBRT[2], $aCoOrdinatesLBRT[3]);
281 else if (isset($aParams['viewbox']) && $aParams['viewbox'])
283 $aCoOrdinatesLTRB = explode(',',$aParams['viewbox']);
284 $this->setViewBox($aCoOrdinatesLTRB[0], $aCoOrdinatesLTRB[3], $aCoOrdinatesLTRB[2], $aCoOrdinatesLTRB[1]);
287 if (isset($aParams['route']) && $aParams['route'] && isset($aParams['routewidth']) && $aParams['routewidth'])
289 $aPoints = explode(',',$aParams['route']);
290 if (sizeof($aPoints) % 2 != 0)
292 userError("Uneven number of points");
297 foreach($aPoints as $i => $fPoint)
301 $aRoute[] = array((float)$fPoint, $fPrevCoord);
305 $fPrevCoord = (float)$fPoint;
308 $this->aRoutePoints = $aRoute;
312 function setQueryFromParams($aParams)
315 $sQuery = (isset($aParams['q'])?trim($aParams['q']):'');
318 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
319 $this->setReverseInPlan(false);
323 $this->setQuery($sQuery);
327 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
329 $sValue = trim($sValue);
330 if (!$sValue) return false;
331 $this->aStructuredQuery[$sKey] = $sValue;
332 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
334 $this->iMinAddressRank = $iNewMinAddressRank;
335 $this->iMaxAddressRank = $iNewMaxAddressRank;
337 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
341 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
343 $this->sQuery = false;
346 $this->iMinAddressRank = 0;
347 $this->iMaxAddressRank = 30;
348 $this->aAddressRankList = array();
350 $this->aStructuredQuery = array();
351 $this->sAllowedTypesSQLList = '';
353 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
354 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
355 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
356 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
357 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
358 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
359 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
361 if (sizeof($this->aStructuredQuery) > 0)
363 $this->sQuery = join(', ', $this->aStructuredQuery);
364 if ($this->iMaxAddressRank < 30)
366 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
371 function fallbackStructuredQuery()
373 if (!$this->aStructuredQuery) return false;
375 $aParams = $this->aStructuredQuery;
377 if (sizeof($aParams) == 1) return false;
379 $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
381 foreach($aOrderToFallback as $sType)
383 if (isset($aParams[$sType]))
385 unset($aParams[$sType]);
386 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
394 function getDetails($aPlaceIDs)
396 if (sizeof($aPlaceIDs) == 0) return array();
398 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
400 // Get the details for display (is this a redundant extra step?)
401 $sPlaceIDs = join(',',$aPlaceIDs);
403 $sImportanceSQL = '';
404 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
405 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
407 $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id, calculated_country_code as country_code,";
408 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
409 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
410 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
411 if ($this->bIncludeExtraTags) $sSQL .= "hstore_to_json(extratags)::text as extra,";
412 if ($this->bIncludeNameDetails) $sSQL .= "hstore_to_json(name)::text as names,";
413 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
414 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
415 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
416 $sSQL .= "(extratags->'place') as extra_place ";
417 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
418 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
419 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
420 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
422 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
423 $sSQL .= "and linked_place_id is null ";
424 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
425 if (!$this->bDeDupe) $sSQL .= ",place_id";
426 $sSQL .= ",langaddress ";
427 $sSQL .= ",placename ";
429 if ($this->bIncludeExtraTags) $sSQL .= ",extratags";
430 if ($this->bIncludeNameDetails) $sSQL .= ",name";
431 $sSQL .= ",extratags->'place' ";
433 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
436 $sSQL .= "select 'T' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
437 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
438 $sSQL .= "null as placename,";
439 $sSQL .= "null as ref,";
440 if ($this->bIncludeExtraTags) $sSQL .= "null as extra,";
441 if ($this->bIncludeNameDetails) $sSQL .= "null as names,";
442 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
443 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
444 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_tiger.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
445 $sSQL .= "null as extra_place ";
446 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
447 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
448 $sSQL .= "group by place_id";
449 if (!$this->bDeDupe) $sSQL .= ",place_id ";
452 $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
453 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
454 $sSQL .= "null as placename,";
455 $sSQL .= "null as ref,";
456 if ($this->bIncludeExtraTags) $sSQL .= "null as extra,";
457 if ($this->bIncludeNameDetails) $sSQL .= "null as names,";
458 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
459 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
460 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
461 $sSQL .= "null as extra_place ";
462 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
463 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
464 $sSQL .= "group by place_id";
465 if (!$this->bDeDupe) $sSQL .= ",place_id";
466 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
470 $sSQL .= " order by importance desc";
471 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
472 $aSearchResults = $this->oDB->getAll($sSQL);
474 if (PEAR::IsError($aSearchResults))
476 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
479 return $aSearchResults;
482 function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
485 Calculate all searches using aValidTokens i.e.
486 'Wodsworth Road, Sheffield' =>
490 0 1 (wodsworth)(road)
493 Score how good the search is so they can be ordered
495 foreach($aPhrases as $iPhrase => $sPhrase)
497 $aNewPhraseSearches = array();
498 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
499 else $sPhraseType = '';
501 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
503 // Too many permutations - too expensive
504 if ($iWordSet > 120) break;
506 $aWordsetSearches = $aSearches;
508 // Add all words from this wordset
509 foreach($aWordset as $iToken => $sToken)
511 //echo "<br><b>$sToken</b>";
512 $aNewWordsetSearches = array();
514 foreach($aWordsetSearches as $aCurrentSearch)
517 //var_dump($aCurrentSearch);
520 // If the token is valid
521 if (isset($aValidTokens[' '.$sToken]))
523 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
525 $aSearch = $aCurrentSearch;
526 $aSearch['iSearchRank']++;
527 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
529 if ($aSearch['sCountryCode'] === false)
531 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
532 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
533 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)))
535 $aSearch['iSearchRank'] += 5;
537 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
540 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
542 if ($aSearch['fLat'] === '')
544 $aSearch['fLat'] = $aSearchTerm['lat'];
545 $aSearch['fLon'] = $aSearchTerm['lon'];
546 $aSearch['fRadius'] = $aSearchTerm['radius'];
547 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
550 elseif ($sPhraseType == 'postalcode')
552 // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
553 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
555 // If we already have a name try putting the postcode first
556 if (sizeof($aSearch['aName']))
558 $aNewSearch = $aSearch;
559 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
560 $aNewSearch['aName'] = array();
561 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
562 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
565 if (sizeof($aSearch['aName']))
567 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
569 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
573 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
574 $aSearch['iSearchRank'] += 1000; // skip;
579 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
580 //$aSearch['iNamePhrase'] = $iPhrase;
582 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
586 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
588 if ($aSearch['sHouseNumber'] === '')
590 $aSearch['sHouseNumber'] = $sToken;
591 // sanity check: if the housenumber is not mainly made
592 // up of numbers, add a penalty
593 if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
594 // also housenumbers should appear in the first or second phrase
595 if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
596 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
598 // Fall back to not searching for this item (better than nothing)
599 $aSearch = $aCurrentSearch;
600 $aSearch['iSearchRank'] += 1;
601 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
605 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
607 if ($aSearch['sClass'] === '')
609 $aSearch['sOperator'] = $aSearchTerm['operator'];
610 $aSearch['sClass'] = $aSearchTerm['class'];
611 $aSearch['sType'] = $aSearchTerm['type'];
612 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
613 else $aSearch['sOperator'] = 'near'; // near = in for the moment
614 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
616 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
619 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
621 if (sizeof($aSearch['aName']))
623 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
625 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
629 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
630 $aSearch['iSearchRank'] += 1000; // skip;
635 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
636 //$aSearch['iNamePhrase'] = $iPhrase;
638 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
642 // Look for partial matches.
643 // Note that there is no point in adding country terms here
644 // because country are omitted in the address.
645 if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country')
647 // Allow searching for a word - but at extra cost
648 foreach($aValidTokens[$sToken] as $aSearchTerm)
650 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
652 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false)
654 $aSearch = $aCurrentSearch;
655 $aSearch['iSearchRank'] += 1;
656 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
658 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
659 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
661 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
663 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
664 $aSearch['iSearchRank'] += 1;
665 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
666 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
668 if (empty($aSearchTermToken['country_code'])
669 && empty($aSearchTermToken['lat'])
670 && empty($aSearchTermToken['class']))
672 $aSearch = $aCurrentSearch;
673 $aSearch['iSearchRank'] += 1;
674 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
675 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
681 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
682 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
683 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
687 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
689 $aSearch = $aCurrentSearch;
690 $aSearch['iSearchRank'] += 1;
691 if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
692 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
693 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
694 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
696 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
697 $aSearch['iNamePhrase'] = $iPhrase;
698 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
705 // Allow skipping a word - but at EXTREAM cost
706 //$aSearch = $aCurrentSearch;
707 //$aSearch['iSearchRank']+=100;
708 //$aNewWordsetSearches[] = $aSearch;
712 usort($aNewWordsetSearches, 'bySearchRank');
713 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
715 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
717 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
718 usort($aNewPhraseSearches, 'bySearchRank');
720 $aSearchHash = array();
721 foreach($aNewPhraseSearches as $iSearch => $aSearch)
723 $sHash = serialize($aSearch);
724 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
725 else $aSearchHash[$sHash] = 1;
728 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
731 // Re-group the searches by their score, junk anything over 20 as just not worth trying
732 $aGroupedSearches = array();
733 foreach($aNewPhraseSearches as $aSearch)
735 if ($aSearch['iSearchRank'] < $this->iMaxRank)
737 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
738 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
741 ksort($aGroupedSearches);
744 $aSearches = array();
745 foreach($aGroupedSearches as $iScore => $aNewSearches)
747 $iSearchCount += sizeof($aNewSearches);
748 $aSearches = array_merge($aSearches, $aNewSearches);
749 if ($iSearchCount > 50) break;
752 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
755 return $aGroupedSearches;
759 /* Perform the actual query lookup.
761 Returns an ordered list of results, each with the following fields:
762 osm_type: type of corresponding OSM object
766 P - postcode (internally computed)
767 osm_id: id of corresponding OSM object
768 class: general object class (corresponds to tag key of primary OSM tag)
769 type: subclass of object (corresponds to tag value of primary OSM tag)
770 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
771 rank_search: rank in search hierarchy
772 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
773 rank_address: rank in address hierarchy (determines orer in address)
774 place_id: internal key (may differ between different instances)
775 country_code: ISO country code
776 langaddress: localized full address
777 placename: localized name of object
778 ref: content of ref tag (if available)
781 importance: importance of place based on Wikipedia link count
782 addressimportance: cumulated importance of address elements
783 extra_place: type of place (for admin boundaries, if there is a place tag)
784 aBoundingBox: bounding Box
785 label: short description of the object class/type (English only)
786 name: full name (currently the same as langaddress)
787 foundorder: secondary ordering for places with same importance
791 if (!$this->sQuery && !$this->aStructuredQuery) return false;
793 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
794 $sCountryCodesSQL = false;
795 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
797 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
800 $sQuery = $this->sQuery;
802 // Conflicts between US state abreviations and various words for 'the' in different languages
803 if (isset($this->aLangPrefOrder['name:en']))
805 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/','\1illinois\2', $sQuery);
806 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/','\1alabama\2', $sQuery);
807 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/','\1louisiana\2', $sQuery);
811 $sViewboxCentreSQL = false;
812 $bBoundingBoxSearch = false;
815 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
816 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
817 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
818 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
819 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
820 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
822 $this->sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$this->aViewBox[0].",".(float)$this->aViewBox[1]."),ST_Point(".(float)$this->aViewBox[2].",".(float)$this->aViewBox[3].")),4326)";
823 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
824 $bBoundingBoxSearch = $this->bBoundedSearch;
828 if ($this->aRoutePoints)
830 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
832 foreach($this->aRoutePoints as $aPoint)
834 if (!$bFirst) $sViewboxCentreSQL .= ",";
835 $sViewboxCentreSQL .= $aPoint[0].' '.$aPoint[1];
838 $sViewboxCentreSQL .= ")'::geometry,4326)";
840 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
841 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
842 if (PEAR::isError($this->sViewboxSmallSQL))
844 failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
846 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
848 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
849 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
850 if (PEAR::isError($this->sViewboxLargeSQL))
852 failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
854 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
855 $bBoundingBoxSearch = $this->bBoundedSearch;
858 // Do we have anything that looks like a lat/lon pair?
859 if ( $aLooksLike = looksLikeLatLonPair($sQuery) ){
860 $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
861 $sQuery = $aLooksLike['query'];
864 $aSearchResults = array();
865 if ($sQuery || $this->aStructuredQuery)
867 // Start with a blank search
869 array('iSearchRank' => 0,
871 'sCountryCode' => false,
873 'aAddress' => array(),
874 'aFullNameAddress' => array(),
875 'aNameNonSearch' => array(),
876 'aAddressNonSearch' => array(),
878 'aFeatureName' => array(),
881 'sHouseNumber' => '',
888 // Do we have a radius search?
889 $sNearPointSQL = false;
890 if ($this->aNearPoint)
892 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
893 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
894 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
895 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
898 // Any 'special' terms in the search?
899 $bSpecialTerms = false;
900 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
901 $aSpecialTerms = array();
902 foreach($aSpecialTermsRaw as $aSpecialTerm)
904 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
905 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
908 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
909 $aSpecialTerms = array();
910 if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity'])
912 $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
913 unset($this->aStructuredQuery['amenity']);
915 foreach($aSpecialTermsRaw as $aSpecialTerm)
917 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
918 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
919 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
920 $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';
921 if (CONST_Debug) var_Dump($sSQL);
922 $aSearchWords = $this->oDB->getAll($sSQL);
923 $aNewSearches = array();
924 foreach($aSearches as $aSearch)
926 foreach($aSearchWords as $aSearchTerm)
928 $aNewSearch = $aSearch;
929 if ($aSearchTerm['country_code'])
931 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
932 $aNewSearches[] = $aNewSearch;
933 $bSpecialTerms = true;
935 if ($aSearchTerm['class'])
937 $aNewSearch['sClass'] = $aSearchTerm['class'];
938 $aNewSearch['sType'] = $aSearchTerm['type'];
939 $aNewSearches[] = $aNewSearch;
940 $bSpecialTerms = true;
944 $aSearches = $aNewSearches;
947 // Split query into phrases
948 // Commas are used to reduce the search space by indicating where phrases split
949 if ($this->aStructuredQuery)
951 $aPhrases = $this->aStructuredQuery;
952 $bStructuredPhrases = true;
956 $aPhrases = explode(',',$sQuery);
957 $bStructuredPhrases = false;
960 // Convert each phrase to standard form
961 // Create a list of standard words
962 // Get all 'sets' of words
963 // Generate a complete list of all
965 foreach($aPhrases as $iPhrase => $sPhrase)
967 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
968 if (PEAR::isError($aPhrase))
970 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
971 if (CONST_Debug) var_dump($aPhrase);
974 if (trim($aPhrase['string']))
976 $aPhrases[$iPhrase] = $aPhrase;
977 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
978 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
979 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
983 unset($aPhrases[$iPhrase]);
987 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
988 $aPhraseTypes = array_keys($aPhrases);
989 $aPhrases = array_values($aPhrases);
991 if (sizeof($aTokens))
993 // Check which tokens we have, get the ID numbers
994 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
995 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
997 if (CONST_Debug) var_Dump($sSQL);
999 $aValidTokens = array();
1000 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
1001 else $aDatabaseWords = array();
1002 if (PEAR::IsError($aDatabaseWords))
1004 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
1006 $aPossibleMainWordIDs = array();
1007 $aWordFrequencyScores = array();
1008 foreach($aDatabaseWords as $aToken)
1010 // Very special case - require 2 letter country param to match the country code found
1011 if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
1012 && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code'])
1017 if (isset($aValidTokens[$aToken['word_token']]))
1019 $aValidTokens[$aToken['word_token']][] = $aToken;
1023 $aValidTokens[$aToken['word_token']] = array($aToken);
1025 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
1026 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
1028 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
1030 // Try and calculate GB postcodes we might be missing
1031 foreach($aTokens as $sToken)
1033 // Source of gb postcodes is now definitive - always use
1034 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
1036 if (substr($aData[1],-2,1) != ' ')
1038 $aData[0] = substr($aData[0],0,strlen($aData[1])-1).' '.substr($aData[0],strlen($aData[1])-1);
1039 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
1041 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
1042 if ($aGBPostcodeLocation)
1044 $aValidTokens[$sToken] = $aGBPostcodeLocation;
1047 // US ZIP+4 codes - if there is no token,
1048 // merge in the 5-digit ZIP code
1049 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
1051 if (isset($aValidTokens[$aData[1]]))
1053 foreach($aValidTokens[$aData[1]] as $aToken)
1055 if (!$aToken['class'])
1057 if (isset($aValidTokens[$sToken]))
1059 $aValidTokens[$sToken][] = $aToken;
1063 $aValidTokens[$sToken] = array($aToken);
1071 foreach($aTokens as $sToken)
1073 // Unknown single word token with a number - assume it is a house number
1074 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
1076 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
1080 // Any words that have failed completely?
1081 // TODO: suggestions
1083 // Start the search process
1084 $aResultPlaceIDs = array();
1086 $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1088 if ($this->bReverseInPlan)
1090 // Reverse phrase array and also reverse the order of the wordsets in
1091 // the first and final phrase. Don't bother about phrases in the middle
1092 // because order in the address doesn't matter.
1093 $aPhrases = array_reverse($aPhrases);
1094 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1095 if (sizeof($aPhrases) > 1)
1097 $aFinalPhrase = end($aPhrases);
1098 $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1100 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1102 foreach($aGroupedSearches as $aSearches)
1104 foreach($aSearches as $aSearch)
1106 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1108 if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1109 $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1115 $aGroupedSearches = $aReverseGroupedSearches;
1116 ksort($aGroupedSearches);
1121 // Re-group the searches by their score, junk anything over 20 as just not worth trying
1122 $aGroupedSearches = array();
1123 foreach($aSearches as $aSearch)
1125 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1127 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1128 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1131 ksort($aGroupedSearches);
1134 if (CONST_Debug) var_Dump($aGroupedSearches);
1136 if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0)
1138 $aCopyGroupedSearches = $aGroupedSearches;
1139 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1141 foreach($aSearches as $iSearch => $aSearch)
1143 $aReductionsList = array($aSearch['aAddress']);
1144 $iSearchRank = $aSearch['iSearchRank'];
1145 while(sizeof($aReductionsList) > 0)
1148 if ($iSearchRank > iMaxRank) break 3;
1149 $aNewReductionsList = array();
1150 foreach($aReductionsList as $aReductionsWordList)
1152 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1154 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1155 $aReverseSearch = $aSearch;
1156 $aSearch['aAddress'] = $aReductionsWordListResult;
1157 $aSearch['iSearchRank'] = $iSearchRank;
1158 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1159 if (sizeof($aReductionsWordListResult) > 0)
1161 $aNewReductionsList[] = $aReductionsWordListResult;
1165 $aReductionsList = $aNewReductionsList;
1169 ksort($aGroupedSearches);
1172 // Filter out duplicate searches
1173 $aSearchHash = array();
1174 foreach($aGroupedSearches as $iGroup => $aSearches)
1176 foreach($aSearches as $iSearch => $aSearch)
1178 $sHash = serialize($aSearch);
1179 if (isset($aSearchHash[$sHash]))
1181 unset($aGroupedSearches[$iGroup][$iSearch]);
1182 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1186 $aSearchHash[$sHash] = 1;
1191 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1195 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1198 foreach($aSearches as $aSearch)
1202 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1203 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1205 // No location term?
1206 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1208 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1210 // Just looking for a country by code - look it up
1211 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1213 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1214 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1215 if ($bBoundingBoxSearch)
1216 $sSQL .= " and _st_intersects($this->sViewboxSmallSQL, geometry)";
1217 $sSQL .= " order by st_area(geometry) desc limit 1";
1218 if (CONST_Debug) var_dump($sSQL);
1219 $aPlaceIDs = $this->oDB->getCol($sSQL);
1223 $aPlaceIDs = array();
1228 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1229 if (!$aSearch['sClass']) continue;
1230 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1231 if ($this->oDB->getOne($sSQL))
1233 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1234 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1235 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1236 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1237 if (sizeof($this->aExcludePlaceIDs))
1239 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1241 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1242 $sSQL .= " limit $this->iLimit";
1243 if (CONST_Debug) var_dump($sSQL);
1244 $aPlaceIDs = $this->oDB->getCol($sSQL);
1246 // If excluded place IDs are given, it is fair to assume that
1247 // there have been results in the small box, so no further
1248 // expansion in that case.
1249 // Also don't expand if bounded results were requested.
1250 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch)
1252 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1253 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1254 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1255 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1256 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1257 $sSQL .= " limit $this->iLimit";
1258 if (CONST_Debug) var_dump($sSQL);
1259 $aPlaceIDs = $this->oDB->getCol($sSQL);
1264 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1265 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1266 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1267 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1268 $sSQL .= " limit $this->iLimit";
1269 if (CONST_Debug) var_dump($sSQL);
1270 $aPlaceIDs = $this->oDB->getCol($sSQL);
1274 // If a coordinate is given, the search must either
1275 // be for a name or a special search. Ignore everythin else.
1276 else if ($aSearch['fLon'] && !sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['sClass'])
1278 $aPlaceIDs = array();
1282 $aPlaceIDs = array();
1284 // First we need a position, either aName or fLat or both
1288 if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress']))
1290 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1291 $aOrder[] = "exists(select place_id from placex where parent_place_id = search_name.place_id and transliteration(housenumber) ~* E'".$sHouseNumberRegex."' limit 1) desc";
1294 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1295 // they might be right - but they are just too darned expensive to run
1296 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1297 //if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1298 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1300 // For infrequent name terms disable index usage for address
1301 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1302 sizeof($aSearch['aName']) == 1 &&
1303 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1305 //$aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1306 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddress'],",")."]";
1310 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1311 //if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1314 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1315 if ($aSearch['sHouseNumber'])
1317 $aTerms[] = "address_rank between 16 and 27";
1321 if ($this->iMinAddressRank > 0)
1323 $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1325 if ($this->iMaxAddressRank < 30)
1327 $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1330 if ($aSearch['fLon'] && $aSearch['fLat'])
1332 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1333 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1335 if (sizeof($this->aExcludePlaceIDs))
1337 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1339 if ($sCountryCodesSQL)
1341 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1344 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1345 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1347 if ($aSearch['sHouseNumber'])
1349 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1353 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1355 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1356 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1358 $aOrder[] = "$sImportanceSQL DESC";
1359 if (sizeof($aSearch['aFullNameAddress']))
1361 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1362 $aOrder[] = 'exactmatch DESC';
1364 $sExactMatchSQL = '0::int as exactmatch';
1367 if (sizeof($aTerms))
1369 $sSQL = "select place_id, ";
1370 $sSQL .= $sExactMatchSQL;
1371 $sSQL .= " from search_name";
1372 $sSQL .= " where ".join(' and ',$aTerms);
1373 $sSQL .= " order by ".join(', ',$aOrder);
1374 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1375 $sSQL .= " limit 20";
1376 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1377 $sSQL .= " limit 1";
1379 $sSQL .= " limit ".$this->iLimit;
1381 if (CONST_Debug) { var_dump($sSQL); }
1382 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1383 if (PEAR::IsError($aViewBoxPlaceIDs))
1385 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1387 //var_dump($aViewBoxPlaceIDs);
1388 // Did we have an viewbox matches?
1389 $aPlaceIDs = array();
1390 $bViewBoxMatch = false;
1391 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1393 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1394 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1395 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1396 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1397 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1398 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1401 //var_Dump($aPlaceIDs);
1404 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1406 $aRoadPlaceIDs = $aPlaceIDs;
1407 $sPlaceIDs = join(',',$aPlaceIDs);
1409 // Now they are indexed look for a house attached to a street we found
1410 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1411 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1412 if (sizeof($this->aExcludePlaceIDs))
1414 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1416 $sSQL .= " limit $this->iLimit";
1417 if (CONST_Debug) var_dump($sSQL);
1418 $aPlaceIDs = $this->oDB->getCol($sSQL);
1420 // If not try the aux fallback table
1422 if (!sizeof($aPlaceIDs))
1424 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1425 if (sizeof($this->aExcludePlaceIDs))
1427 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1429 //$sSQL .= " limit $this->iLimit";
1430 if (CONST_Debug) var_dump($sSQL);
1431 $aPlaceIDs = $this->oDB->getCol($sSQL);
1435 if (!sizeof($aPlaceIDs))
1437 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1438 if (sizeof($this->aExcludePlaceIDs))
1440 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1442 //$sSQL .= " limit $this->iLimit";
1443 if (CONST_Debug) var_dump($sSQL);
1444 $aPlaceIDs = $this->oDB->getCol($sSQL);
1447 // Fallback to the road
1448 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1450 $aPlaceIDs = $aRoadPlaceIDs;
1455 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1457 $sPlaceIDs = join(',',$aPlaceIDs);
1458 $aClassPlaceIDs = array();
1460 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1462 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1463 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1464 $sSQL .= " and linked_place_id is null";
1465 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1466 $sSQL .= " order by rank_search asc limit $this->iLimit";
1467 if (CONST_Debug) var_dump($sSQL);
1468 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1471 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1473 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1474 $bCacheTable = $this->oDB->getOne($sSQL);
1476 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1478 if (CONST_Debug) var_dump($sSQL);
1479 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1481 // For state / country level searches the normal radius search doesn't work very well
1482 $sPlaceGeom = false;
1483 if ($this->iMaxRank < 9 && $bCacheTable)
1485 // Try and get a polygon to search in instead
1486 $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";
1487 if (CONST_Debug) var_dump($sSQL);
1488 $sPlaceGeom = $this->oDB->getOne($sSQL);
1497 $this->iMaxRank += 5;
1498 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1499 if (CONST_Debug) var_dump($sSQL);
1500 $aPlaceIDs = $this->oDB->getCol($sSQL);
1501 $sPlaceIDs = join(',',$aPlaceIDs);
1504 if ($sPlaceIDs || $sPlaceGeom)
1510 // More efficient - can make the range bigger
1514 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1515 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1516 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1518 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1519 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1522 $sSQL .= ",placex as f where ";
1523 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1528 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1530 if (sizeof($this->aExcludePlaceIDs))
1532 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1534 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1535 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1536 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1537 $sSQL .= " limit $this->iLimit";
1538 if (CONST_Debug) var_dump($sSQL);
1539 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1543 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1546 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1547 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1549 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1550 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1551 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1552 if (sizeof($this->aExcludePlaceIDs))
1554 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1556 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1557 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1558 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1559 $sSQL .= " limit $this->iLimit";
1560 if (CONST_Debug) var_dump($sSQL);
1561 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1566 $aPlaceIDs = $aClassPlaceIDs;
1572 if (PEAR::IsError($aPlaceIDs))
1574 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1577 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1579 foreach($aPlaceIDs as $iPlaceID)
1581 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1583 if ($iQueryLoop > 20) break;
1586 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1588 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1589 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1590 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1591 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1592 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1593 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1594 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1595 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1597 if (CONST_Debug) var_dump($sSQL);
1598 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1602 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1603 if ($iGroupLoop > 4) break;
1604 if ($iQueryLoop > 30) break;
1607 // Did we find anything?
1608 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1610 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1616 // Just interpret as a reverse geocode
1617 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1619 $aSearchResults = $this->getDetails(array($iPlaceID));
1621 $aSearchResults = array();
1625 if (!sizeof($aSearchResults))
1627 if ($this->bFallback)
1629 if ($this->fallbackStructuredQuery())
1631 return $this->lookup();
1638 $aClassType = getClassTypesWithImportance();
1639 $aRecheckWords = preg_split('/\b[\s,\\-]*/u',$sQuery);
1640 foreach($aRecheckWords as $i => $sWord)
1642 if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1645 if (CONST_Debug) { echo '<i>Recheck words:<\i>'; var_dump($aRecheckWords); }
1647 foreach($aSearchResults as $iResNum => $aResult)
1650 $fDiameter = getResultDiameter($aResult);
1652 $oPlaceLookup = new PlaceLookup($this->oDB);
1653 $oPlaceLookup->setIncludePolygonAsPoints($this->bIncludePolygonAsPoints);
1654 $oPlaceLookup->setIncludePolygonAsText($this->bIncludePolygonAsText);
1655 $oPlaceLookup->setIncludePolygonAsGeoJSON($this->bIncludePolygonAsGeoJSON);
1656 $oPlaceLookup->setIncludePolygonAsKML($this->bIncludePolygonAsKML);
1657 $oPlaceLookup->setIncludePolygonAsSVG($this->bIncludePolygonAsSVG);
1658 $oPlaceLookup->setPolygonSimplificationThreshold($this->fPolygonSimplificationThreshold);
1660 $aOutlineResult = $oPlaceLookup->getOutlines($aResult['place_id'], $aResult['lon'], $aResult['lat'], $fDiameter/2);
1661 if ($aOutlineResult)
1663 $aResult = array_merge($aResult, $aOutlineResult);
1666 if ($aResult['extra_place'] == 'city')
1668 $aResult['class'] = 'place';
1669 $aResult['type'] = 'city';
1670 $aResult['rank_search'] = 16;
1673 // Is there an icon set for this type of result?
1674 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1675 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1677 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1680 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1681 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1683 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1685 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1686 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1688 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1691 if ($this->bIncludeAddressDetails)
1693 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1694 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1696 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1700 if ($this->bIncludeExtraTags)
1702 if ($aResult['extra'])
1704 $aResult['sExtraTags'] = json_decode($aResult['extra']);
1708 $aResult['sExtraTags'] = (object) array();
1712 if ($this->bIncludeNameDetails)
1714 if ($aResult['names'])
1716 $aResult['sNameDetails'] = json_decode($aResult['names']);
1720 $aResult['sNameDetails'] = (object) array();
1724 // Adjust importance for the number of exact string matches in the result
1725 $aResult['importance'] = max(0.001,$aResult['importance']);
1727 $sAddress = $aResult['langaddress'];
1728 foreach($aRecheckWords as $i => $sWord)
1730 if (stripos($sAddress, $sWord)!==false)
1733 if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1737 $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
1739 $aResult['name'] = $aResult['langaddress'];
1740 // secondary ordering (for results with same importance (the smaller the better):
1741 // - approximate importance of address parts
1742 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1743 // - number of exact matches from the query
1744 if (isset($this->exactMatchCache[$aResult['place_id']]))
1745 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1746 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1747 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1748 // - importance of the class/type
1749 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1750 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1752 $aResult['foundorder'] += 0.0001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1756 $aResult['foundorder'] += 0.01;
1758 if (CONST_Debug) { var_dump($aResult); }
1759 $aSearchResults[$iResNum] = $aResult;
1761 uasort($aSearchResults, 'byImportance');
1763 $aOSMIDDone = array();
1764 $aClassTypeNameDone = array();
1765 $aToFilter = $aSearchResults;
1766 $aSearchResults = array();
1769 foreach($aToFilter as $iResNum => $aResult)
1771 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1774 $fLat = $aResult['lat'];
1775 $fLon = $aResult['lon'];
1776 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1779 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1780 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1782 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1783 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1784 $aSearchResults[] = $aResult;
1787 // Absolute limit on number of results
1788 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1791 return $aSearchResults;