6 protected $aLangPrefOrder = array();
8 protected $bIncludeAddressDetails = false;
10 protected $bIncludePolygonAsPoints = false;
11 protected $bIncludePolygonAsText = false;
12 protected $bIncludePolygonAsGeoJSON = false;
13 protected $bIncludePolygonAsKML = false;
14 protected $bIncludePolygonAsSVG = false;
16 protected $aExcludePlaceIDs = array();
17 protected $bDeDupe = true;
18 protected $bReverseInPlan = false;
20 protected $iLimit = 20;
21 protected $iFinalLimit = 10;
22 protected $iOffset = 0;
23 protected $bFallback = false;
25 protected $aCountryCodes = false;
26 protected $aNearPoint = false;
28 protected $bBoundedSearch = false;
29 protected $aViewBox = false;
30 protected $sViewboxSmallSQL = false;
31 protected $sViewboxLargeSQL = false;
32 protected $aRoutePoints = false;
34 protected $iMaxRank = 20;
35 protected $iMinAddressRank = 0;
36 protected $iMaxAddressRank = 30;
37 protected $aAddressRankList = array();
38 protected $exactMatchCache = array();
40 protected $sAllowedTypesSQLList = false;
42 protected $sQuery = false;
43 protected $aStructuredQuery = false;
45 function Geocode(&$oDB)
50 function setReverseInPlan($bReverse)
52 $this->bReverseInPlan = $bReverse;
55 function setLanguagePreference($aLangPref)
57 $this->aLangPrefOrder = $aLangPref;
60 function setIncludeAddressDetails($bAddressDetails = true)
62 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
65 function getIncludeAddressDetails()
67 return $this->bIncludeAddressDetails;
70 function setIncludePolygonAsPoints($b = true)
72 $this->bIncludePolygonAsPoints = $b;
75 function getIncludePolygonAsPoints()
77 return $this->bIncludePolygonAsPoints;
80 function setIncludePolygonAsText($b = true)
82 $this->bIncludePolygonAsText = $b;
85 function getIncludePolygonAsText()
87 return $this->bIncludePolygonAsText;
90 function setIncludePolygonAsGeoJSON($b = true)
92 $this->bIncludePolygonAsGeoJSON = $b;
95 function setIncludePolygonAsKML($b = true)
97 $this->bIncludePolygonAsKML = $b;
100 function setIncludePolygonAsSVG($b = true)
102 $this->bIncludePolygonAsSVG = $b;
105 function setDeDupe($bDeDupe = true)
107 $this->bDeDupe = (bool)$bDeDupe;
110 function setLimit($iLimit = 10)
112 if ($iLimit > 50) $iLimit = 50;
113 if ($iLimit < 1) $iLimit = 1;
115 $this->iFinalLimit = $iLimit;
116 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
119 function setOffset($iOffset = 0)
121 $this->iOffset = $iOffset;
124 function setFallback($bFallback = true)
126 $this->bFallback = (bool)$bFallback;
129 function setExcludedPlaceIDs($a)
131 // TODO: force to int
132 $this->aExcludePlaceIDs = $a;
135 function getExcludedPlaceIDs()
137 return $this->aExcludePlaceIDs;
140 function setBounded($bBoundedSearch = true)
142 $this->bBoundedSearch = (bool)$bBoundedSearch;
145 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
147 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
150 function getViewBoxString()
152 if (!$this->aViewBox) return null;
153 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
156 function setRoute($aRoutePoints)
158 $this->aRoutePoints = $aRoutePoints;
161 function setFeatureType($sFeatureType)
163 switch($sFeatureType)
166 $this->setRankRange(4, 4);
169 $this->setRankRange(8, 8);
172 $this->setRankRange(14, 16);
175 $this->setRankRange(8, 20);
180 function setRankRange($iMin, $iMax)
182 $this->iMinAddressRank = (int)$iMin;
183 $this->iMaxAddressRank = (int)$iMax;
186 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
188 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
191 function setCountryCodesList($aCountryCodes)
193 $this->aCountryCodes = $aCountryCodes;
196 function setQuery($sQueryString)
198 $this->sQuery = $sQueryString;
199 $this->aStructuredQuery = false;
202 function getQueryString()
204 return $this->sQuery;
208 function loadParamArray($aParams)
210 if (isset($aParams['addressdetails'])) $this->bIncludeAddressDetails = (bool)$aParams['addressdetails'];
211 if (isset($aParams['bounded'])) $this->bBoundedSearch = (bool)$aParams['bounded'];
212 if (isset($aParams['dedupe'])) $this->bDeDupe = (bool)$aParams['dedupe'];
214 if (isset($aParams['limit'])) $this->setLimit((int)$aParams['limit']);
215 if (isset($aParams['offset'])) $this->iOffset = (int)$aParams['offset'];
217 if (isset($aParams['fallback'])) $this->bFallback = (bool)$aParams['fallback'];
219 // List of excluded Place IDs - used for more acurate pageing
220 if (isset($aParams['exclude_place_ids']) && $aParams['exclude_place_ids'])
222 foreach(explode(',',$aParams['exclude_place_ids']) as $iExcludedPlaceID)
224 $iExcludedPlaceID = (int)$iExcludedPlaceID;
225 if ($iExcludedPlaceID) $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
227 $this->aExcludePlaceIDs = $aExcludePlaceIDs;
230 // Only certain ranks of feature
231 if (isset($aParams['featureType'])) $this->setFeatureType($aParams['featureType']);
232 if (isset($aParams['featuretype'])) $this->setFeatureType($aParams['featuretype']);
235 if (isset($aParams['countrycodes']))
237 $aCountryCodes = array();
238 foreach(explode(',',$aParams['countrycodes']) as $sCountryCode)
240 if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
242 $aCountryCodes[] = strtolower($sCountryCode);
245 $this->aCountryCodes = $aCountryCodes;
248 if (isset($aParams['viewboxlbrt']) && $aParams['viewboxlbrt'])
250 $aCoOrdinatesLBRT = explode(',',$aParams['viewboxlbrt']);
251 $this->setViewBox($aCoOrdinatesLBRT[0], $aCoOrdinatesLBRT[1], $aCoOrdinatesLBRT[2], $aCoOrdinatesLBRT[3]);
253 else if (isset($aParams['viewbox']) && $aParams['viewbox'])
255 $aCoOrdinatesLTRB = explode(',',$aParams['viewbox']);
256 $this->setViewBox($aCoOrdinatesLTRB[0], $aCoOrdinatesLTRB[3], $aCoOrdinatesLTRB[2], $aCoOrdinatesLTRB[1]);
259 if (isset($aParams['route']) && $aParams['route'] && isset($aParams['routewidth']) && $aParams['routewidth'])
261 $aPoints = explode(',',$aParams['route']);
262 if (sizeof($aPoints) % 2 != 0)
264 userError("Uneven number of points");
269 foreach($aPoints as $i => $fPoint)
273 $aRoute[] = array((float)$fPoint, $fPrevCoord);
277 $fPrevCoord = (float)$fPoint;
280 $this->aRoutePoints = $aRoute;
284 function setQueryFromParams($aParams)
287 $sQuery = (isset($aParams['q'])?trim($aParams['q']):'');
290 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
291 $this->setReverseInPlan(false);
295 $this->setQuery($sQuery);
299 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
301 $sValue = trim($sValue);
302 if (!$sValue) return false;
303 $this->aStructuredQuery[$sKey] = $sValue;
304 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
306 $this->iMinAddressRank = $iNewMinAddressRank;
307 $this->iMaxAddressRank = $iNewMaxAddressRank;
309 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
313 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
315 $this->sQuery = false;
318 $this->iMinAddressRank = 0;
319 $this->iMaxAddressRank = 30;
320 $this->aAddressRankList = array();
322 $this->aStructuredQuery = array();
323 $this->sAllowedTypesSQLList = '';
325 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
326 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
327 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
328 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
329 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
330 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
331 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
333 if (sizeof($this->aStructuredQuery) > 0)
335 $this->sQuery = join(', ', $this->aStructuredQuery);
336 if ($this->iMaxAddressRank < 30)
338 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
343 function fallbackStructuredQuery()
345 if (!$this->aStructuredQuery) return false;
347 $aParams = $this->aStructuredQuery;
349 if (sizeof($aParams) == 1) return false;
351 $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
353 foreach($aOrderToFallback as $sType)
355 if (isset($aParams[$sType]))
357 unset($aParams[$sType]);
358 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
366 function getDetails($aPlaceIDs)
368 if (sizeof($aPlaceIDs) == 0) return array();
370 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
372 // Get the details for display (is this a redundant extra step?)
373 $sPlaceIDs = join(',',$aPlaceIDs);
375 $sImportanceSQL = '';
376 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
377 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
379 $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,";
380 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
381 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
382 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
383 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
384 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
385 $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, ";
386 $sSQL .= "(extratags->'place') as extra_place ";
387 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
388 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
389 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
390 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
392 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
393 $sSQL .= "and linked_place_id is null ";
394 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
395 if (!$this->bDeDupe) $sSQL .= ",place_id";
396 $sSQL .= ",langaddress ";
397 $sSQL .= ",placename ";
399 $sSQL .= ",extratags->'place' ";
401 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
404 $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,";
405 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
406 $sSQL .= "null as placename,";
407 $sSQL .= "null as ref,";
408 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
409 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
410 $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, ";
411 $sSQL .= "null as extra_place ";
412 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
413 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
414 $sSQL .= "group by place_id";
415 if (!$this->bDeDupe) $sSQL .= ",place_id ";
417 $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,";
418 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
419 $sSQL .= "null as placename,";
420 $sSQL .= "null as ref,";
421 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
422 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
423 $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, ";
424 $sSQL .= "null as extra_place ";
425 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
426 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
427 $sSQL .= "group by place_id";
428 if (!$this->bDeDupe) $sSQL .= ",place_id";
429 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
432 $sSQL .= " order by importance desc";
433 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
434 $aSearchResults = $this->oDB->getAll($sSQL);
436 if (PEAR::IsError($aSearchResults))
438 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
441 return $aSearchResults;
444 function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
447 Calculate all searches using aValidTokens i.e.
448 'Wodsworth Road, Sheffield' =>
452 0 1 (wodsworth)(road)
455 Score how good the search is so they can be ordered
457 foreach($aPhrases as $iPhrase => $sPhrase)
459 $aNewPhraseSearches = array();
460 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
461 else $sPhraseType = '';
463 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
465 // Too many permutations - too expensive
466 if ($iWordSet > 120) break;
468 $aWordsetSearches = $aSearches;
470 // Add all words from this wordset
471 foreach($aWordset as $iToken => $sToken)
473 //echo "<br><b>$sToken</b>";
474 $aNewWordsetSearches = array();
476 foreach($aWordsetSearches as $aCurrentSearch)
479 //var_dump($aCurrentSearch);
482 // If the token is valid
483 if (isset($aValidTokens[' '.$sToken]))
485 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
487 $aSearch = $aCurrentSearch;
488 $aSearch['iSearchRank']++;
489 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
491 if ($aSearch['sCountryCode'] === false)
493 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
494 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
495 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)))
497 $aSearch['iSearchRank'] += 5;
499 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
502 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
504 if ($aSearch['fLat'] === '')
506 $aSearch['fLat'] = $aSearchTerm['lat'];
507 $aSearch['fLon'] = $aSearchTerm['lon'];
508 $aSearch['fRadius'] = $aSearchTerm['radius'];
509 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
512 elseif ($sPhraseType == 'postalcode')
514 // 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
515 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
517 // If we already have a name try putting the postcode first
518 if (sizeof($aSearch['aName']))
520 $aNewSearch = $aSearch;
521 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
522 $aNewSearch['aName'] = array();
523 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
524 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
527 if (sizeof($aSearch['aName']))
529 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
531 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
535 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
536 $aSearch['iSearchRank'] += 1000; // skip;
541 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
542 //$aSearch['iNamePhrase'] = $iPhrase;
544 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
548 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
550 if ($aSearch['sHouseNumber'] === '')
552 $aSearch['sHouseNumber'] = $sToken;
553 // sanity check: if the housenumber is not mainly made
554 // up of numbers, add a penalty
555 if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
556 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
558 // Fall back to not searching for this item (better than nothing)
559 $aSearch = $aCurrentSearch;
560 $aSearch['iSearchRank'] += 1;
561 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
565 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
567 if ($aSearch['sClass'] === '')
569 $aSearch['sOperator'] = $aSearchTerm['operator'];
570 $aSearch['sClass'] = $aSearchTerm['class'];
571 $aSearch['sType'] = $aSearchTerm['type'];
572 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
573 else $aSearch['sOperator'] = 'near'; // near = in for the moment
574 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
576 // Do we have a shortcut id?
577 if ($aSearch['sOperator'] == 'name')
579 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
580 if ($iAmenityID = $this->oDB->getOne($sSQL))
582 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
583 $aSearch['aName'][$iAmenityID] = $iAmenityID;
584 $aSearch['sClass'] = '';
585 $aSearch['sType'] = '';
588 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
591 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
593 if (sizeof($aSearch['aName']))
595 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
597 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
601 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
602 $aSearch['iSearchRank'] += 1000; // skip;
607 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
608 //$aSearch['iNamePhrase'] = $iPhrase;
610 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
614 if (isset($aValidTokens[$sToken]))
616 // Allow searching for a word - but at extra cost
617 foreach($aValidTokens[$sToken] as $aSearchTerm)
619 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
621 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false)
623 $aSearch = $aCurrentSearch;
624 $aSearch['iSearchRank'] += 1;
625 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
627 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
628 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
630 elseif (isset($aValidTokens[' '.$sToken]) && strlen($sToken) >= 4) // revert to the token version?
632 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
633 $aSearch['iSearchRank'] += 1;
634 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
635 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
637 if (empty($aSearchTermToken['country_code'])
638 && empty($aSearchTermToken['lat'])
639 && empty($aSearchTermToken['class']))
641 $aSearch = $aCurrentSearch;
642 $aSearch['iSearchRank'] += 1;
643 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
644 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
650 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
651 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
652 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
656 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
658 $aSearch = $aCurrentSearch;
659 $aSearch['iSearchRank'] += 1;
660 if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
661 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
662 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
663 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
665 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
666 $aSearch['iNamePhrase'] = $iPhrase;
667 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
674 // Allow skipping a word - but at EXTREAM cost
675 //$aSearch = $aCurrentSearch;
676 //$aSearch['iSearchRank']+=100;
677 //$aNewWordsetSearches[] = $aSearch;
681 usort($aNewWordsetSearches, 'bySearchRank');
682 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
684 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
686 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
687 usort($aNewPhraseSearches, 'bySearchRank');
689 $aSearchHash = array();
690 foreach($aNewPhraseSearches as $iSearch => $aSearch)
692 $sHash = serialize($aSearch);
693 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
694 else $aSearchHash[$sHash] = 1;
697 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
700 // Re-group the searches by their score, junk anything over 20 as just not worth trying
701 $aGroupedSearches = array();
702 foreach($aNewPhraseSearches as $aSearch)
704 if ($aSearch['iSearchRank'] < $this->iMaxRank)
706 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
707 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
710 ksort($aGroupedSearches);
713 $aSearches = array();
714 foreach($aGroupedSearches as $iScore => $aNewSearches)
716 $iSearchCount += sizeof($aNewSearches);
717 $aSearches = array_merge($aSearches, $aNewSearches);
718 if ($iSearchCount > 50) break;
721 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
724 return $aGroupedSearches;
728 /* Perform the actual query lookup.
730 Returns an ordered list of results, each with the following fields:
731 osm_type: type of corresponding OSM object
735 P - postcode (internally computed)
736 osm_id: id of corresponding OSM object
737 class: general object class (corresponds to tag key of primary OSM tag)
738 type: subclass of object (corresponds to tag value of primary OSM tag)
739 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
740 rank_search: rank in search hierarchy
741 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
742 rank_address: rank in address hierarchy (determines orer in address)
743 place_id: internal key (may differ between different instances)
744 country_code: ISO country code
745 langaddress: localized full address
746 placename: localized name of object
747 ref: content of ref tag (if available)
750 importance: importance of place based on Wikipedia link count
751 addressimportance: cumulated importance of address elements
752 extra_place: type of place (for admin boundaries, if there is a place tag)
753 aBoundingBox: bounding Box
754 label: short description of the object class/type (English only)
755 name: full name (currently the same as langaddress)
756 foundorder: secondary ordering for places with same importance
760 if (!$this->sQuery && !$this->aStructuredQuery) return false;
762 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
764 $sCountryCodesSQL = false;
765 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
767 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
770 $sQuery = $this->sQuery;
772 // Conflicts between US state abreviations and various words for 'the' in different languages
773 if (isset($this->aLangPrefOrder['name:en']))
775 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/','\1illinois\2', $sQuery);
776 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/','\1alabama\2', $sQuery);
777 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/','\1louisiana\2', $sQuery);
781 $sViewboxCentreSQL = false;
782 $bBoundingBoxSearch = false;
785 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
786 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
787 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
788 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
789 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
790 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
792 $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)";
793 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
794 $bBoundingBoxSearch = $this->bBoundedSearch;
798 if ($this->aRoutePoints)
800 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
802 foreach($this->aRoutePoints as $aPoint)
804 if (!$bFirst) $sViewboxCentreSQL .= ",";
805 $sViewboxCentreSQL .= $aPoint[0].' '.$aPoint[1];
808 $sViewboxCentreSQL .= ")'::geometry,4326)";
810 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
811 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
812 if (PEAR::isError($this->sViewboxSmallSQL))
814 failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
816 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
818 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
819 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
820 if (PEAR::isError($this->sViewboxLargeSQL))
822 failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
824 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
825 $bBoundingBoxSearch = $this->bBoundedSearch;
828 // Do we have anything that looks like a lat/lon pair?
829 if ( $aLooksLike = looksLikeLatLonPair($sQuery) ){
830 $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
831 $sQuery = $aLooksLike['query'];
834 $aSearchResults = array();
835 if ($sQuery || $this->aStructuredQuery)
837 // Start with a blank search
839 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
840 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
841 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
844 // Do we have a radius search?
845 $sNearPointSQL = false;
846 if ($this->aNearPoint)
848 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
849 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
850 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
851 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
854 // Any 'special' terms in the search?
855 $bSpecialTerms = false;
856 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
857 $aSpecialTerms = array();
858 foreach($aSpecialTermsRaw as $aSpecialTerm)
860 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
861 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
864 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
865 $aSpecialTerms = array();
866 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
868 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
869 unset($aStructuredQuery['amenity']);
871 foreach($aSpecialTermsRaw as $aSpecialTerm)
873 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
874 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
875 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
876 $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';
877 if (CONST_Debug) var_Dump($sSQL);
878 $aSearchWords = $this->oDB->getAll($sSQL);
879 $aNewSearches = array();
880 foreach($aSearches as $aSearch)
882 foreach($aSearchWords as $aSearchTerm)
884 $aNewSearch = $aSearch;
885 if ($aSearchTerm['country_code'])
887 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
888 $aNewSearches[] = $aNewSearch;
889 $bSpecialTerms = true;
891 if ($aSearchTerm['class'])
893 $aNewSearch['sClass'] = $aSearchTerm['class'];
894 $aNewSearch['sType'] = $aSearchTerm['type'];
895 $aNewSearches[] = $aNewSearch;
896 $bSpecialTerms = true;
900 $aSearches = $aNewSearches;
903 // Split query into phrases
904 // Commas are used to reduce the search space by indicating where phrases split
905 if ($this->aStructuredQuery)
907 $aPhrases = $this->aStructuredQuery;
908 $bStructuredPhrases = true;
912 $aPhrases = explode(',',$sQuery);
913 $bStructuredPhrases = false;
916 // Convert each phrase to standard form
917 // Create a list of standard words
918 // Get all 'sets' of words
919 // Generate a complete list of all
921 foreach($aPhrases as $iPhrase => $sPhrase)
923 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
924 if (PEAR::isError($aPhrase))
926 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
927 if (CONST_Debug) var_dump($aPhrase);
930 if (trim($aPhrase['string']))
932 $aPhrases[$iPhrase] = $aPhrase;
933 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
934 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
935 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
939 unset($aPhrases[$iPhrase]);
943 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
944 $aPhraseTypes = array_keys($aPhrases);
945 $aPhrases = array_values($aPhrases);
947 if (sizeof($aTokens))
949 // Check which tokens we have, get the ID numbers
950 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
951 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
953 if (CONST_Debug) var_Dump($sSQL);
955 $aValidTokens = array();
956 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
957 else $aDatabaseWords = array();
958 if (PEAR::IsError($aDatabaseWords))
960 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
962 $aPossibleMainWordIDs = array();
963 $aWordFrequencyScores = array();
964 foreach($aDatabaseWords as $aToken)
966 // Very special case - require 2 letter country param to match the country code found
967 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
968 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
973 if (isset($aValidTokens[$aToken['word_token']]))
975 $aValidTokens[$aToken['word_token']][] = $aToken;
979 $aValidTokens[$aToken['word_token']] = array($aToken);
981 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
982 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
984 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
986 // Try and calculate GB postcodes we might be missing
987 foreach($aTokens as $sToken)
989 // Source of gb postcodes is now definitive - always use
990 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
992 if (substr($aData[1],-2,1) != ' ')
994 $aData[0] = substr($aData[0],0,strlen($aData[1])-1).' '.substr($aData[0],strlen($aData[1])-1);
995 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
997 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
998 if ($aGBPostcodeLocation)
1000 $aValidTokens[$sToken] = $aGBPostcodeLocation;
1003 // US ZIP+4 codes - if there is no token,
1004 // merge in the 5-digit ZIP code
1005 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
1007 if (isset($aValidTokens[$aData[1]]))
1009 foreach($aValidTokens[$aData[1]] as $aToken)
1011 if (!$aToken['class'])
1013 if (isset($aValidTokens[$sToken]))
1015 $aValidTokens[$sToken][] = $aToken;
1019 $aValidTokens[$sToken] = array($aToken);
1027 foreach($aTokens as $sToken)
1029 // Unknown single word token with a number - assume it is a house number
1030 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
1032 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
1036 // Any words that have failed completely?
1037 // TODO: suggestions
1039 // Start the search process
1040 $aResultPlaceIDs = array();
1042 $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1044 if ($this->bReverseInPlan)
1046 // Reverse phrase array and also reverse the order of the wordsets in
1047 // the first and final phrase. Don't bother about phrases in the middle
1048 // because order in the address doesn't matter.
1049 $aPhrases = array_reverse($aPhrases);
1050 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1051 if (sizeof($aPhrases) > 1)
1053 $aFinalPhrase = end($aPhrases);
1054 $aFinalPhrase['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1056 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1058 foreach($aGroupedSearches as $aSearches)
1060 foreach($aSearches as $aSearch)
1062 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1064 if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1065 $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1071 $aGroupedSearches = $aReverseGroupedSearches;
1072 ksort($aGroupedSearches);
1077 // Re-group the searches by their score, junk anything over 20 as just not worth trying
1078 $aGroupedSearches = array();
1079 foreach($aSearches as $aSearch)
1081 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1083 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1084 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1087 ksort($aGroupedSearches);
1090 if (CONST_Debug) var_Dump($aGroupedSearches);
1092 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
1094 $aCopyGroupedSearches = $aGroupedSearches;
1095 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1097 foreach($aSearches as $iSearch => $aSearch)
1099 $aReductionsList = array($aSearch['aAddress']);
1100 $iSearchRank = $aSearch['iSearchRank'];
1101 while(sizeof($aReductionsList) > 0)
1104 if ($iSearchRank > iMaxRank) break 3;
1105 $aNewReductionsList = array();
1106 foreach($aReductionsList as $aReductionsWordList)
1108 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1110 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1111 $aReverseSearch = $aSearch;
1112 $aSearch['aAddress'] = $aReductionsWordListResult;
1113 $aSearch['iSearchRank'] = $iSearchRank;
1114 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1115 if (sizeof($aReductionsWordListResult) > 0)
1117 $aNewReductionsList[] = $aReductionsWordListResult;
1121 $aReductionsList = $aNewReductionsList;
1125 ksort($aGroupedSearches);
1128 // Filter out duplicate searches
1129 $aSearchHash = array();
1130 foreach($aGroupedSearches as $iGroup => $aSearches)
1132 foreach($aSearches as $iSearch => $aSearch)
1134 $sHash = serialize($aSearch);
1135 if (isset($aSearchHash[$sHash]))
1137 unset($aGroupedSearches[$iGroup][$iSearch]);
1138 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1142 $aSearchHash[$sHash] = 1;
1147 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1151 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1154 foreach($aSearches as $aSearch)
1158 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1159 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1161 // No location term?
1162 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1164 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1166 // Just looking for a country by code - look it up
1167 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1169 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1170 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1171 if ($bBoundingBoxSearch)
1172 $sSQL .= " and st_overlaps($this->sViewboxSmallSQL, geometry)";
1173 $sSQL .= " order by st_area(geometry) desc limit 1";
1174 if (CONST_Debug) var_dump($sSQL);
1175 $aPlaceIDs = $this->oDB->getCol($sSQL);
1179 $aPlaceIDs = array();
1184 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1185 if (!$aSearch['sClass']) continue;
1186 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1187 if ($this->oDB->getOne($sSQL))
1189 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1190 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1191 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1192 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1193 if (sizeof($this->aExcludePlaceIDs))
1195 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1197 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1198 $sSQL .= " limit $this->iLimit";
1199 if (CONST_Debug) var_dump($sSQL);
1200 $aPlaceIDs = $this->oDB->getCol($sSQL);
1202 // If excluded place IDs are given, it is fair to assume that
1203 // there have been results in the small box, so no further
1204 // expansion in that case.
1205 // Also don't expand if bounded results were requested.
1206 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch)
1208 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1209 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1210 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1211 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1212 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1213 $sSQL .= " limit $this->iLimit";
1214 if (CONST_Debug) var_dump($sSQL);
1215 $aPlaceIDs = $this->oDB->getCol($sSQL);
1220 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1221 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1222 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1223 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1224 $sSQL .= " limit $this->iLimit";
1225 if (CONST_Debug) var_dump($sSQL);
1226 $aPlaceIDs = $this->oDB->getCol($sSQL);
1232 $aPlaceIDs = array();
1234 // First we need a position, either aName or fLat or both
1238 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1239 // they might be right - but they are just too darned expensive to run
1240 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1241 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1242 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1244 // For infrequent name terms disable index usage for address
1245 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1246 sizeof($aSearch['aName']) == 1 &&
1247 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1249 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1253 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1254 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1257 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1258 if ($aSearch['sHouseNumber'])
1260 $aTerms[] = "address_rank between 16 and 27";
1264 if ($this->iMinAddressRank > 0)
1266 $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1268 if ($this->iMaxAddressRank < 30)
1270 $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1273 if ($aSearch['fLon'] && $aSearch['fLat'])
1275 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1276 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1278 if (sizeof($this->aExcludePlaceIDs))
1280 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1282 if ($sCountryCodesSQL)
1284 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1287 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1288 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1290 if ($aSearch['sHouseNumber'])
1292 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1296 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1298 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1299 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1301 $aOrder[] = "$sImportanceSQL DESC";
1302 if (sizeof($aSearch['aFullNameAddress']))
1304 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1305 $aOrder[] = 'exactmatch DESC';
1307 $sExactMatchSQL = '0::int as exactmatch';
1310 if (sizeof($aTerms))
1312 $sSQL = "select place_id, ";
1313 $sSQL .= $sExactMatchSQL;
1314 $sSQL .= " from search_name";
1315 $sSQL .= " where ".join(' and ',$aTerms);
1316 $sSQL .= " order by ".join(', ',$aOrder);
1317 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1318 $sSQL .= " limit 50";
1319 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1320 $sSQL .= " limit 1";
1322 $sSQL .= " limit ".$this->iLimit;
1324 if (CONST_Debug) { var_dump($sSQL); }
1325 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1326 if (PEAR::IsError($aViewBoxPlaceIDs))
1328 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1330 //var_dump($aViewBoxPlaceIDs);
1331 // Did we have an viewbox matches?
1332 $aPlaceIDs = array();
1333 $bViewBoxMatch = false;
1334 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1336 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1337 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1338 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1339 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1340 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1341 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1344 //var_Dump($aPlaceIDs);
1347 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1349 $aRoadPlaceIDs = $aPlaceIDs;
1350 $sPlaceIDs = join(',',$aPlaceIDs);
1352 // Now they are indexed look for a house attached to a street we found
1353 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1354 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1355 if (sizeof($this->aExcludePlaceIDs))
1357 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1359 $sSQL .= " limit $this->iLimit";
1360 if (CONST_Debug) var_dump($sSQL);
1361 $aPlaceIDs = $this->oDB->getCol($sSQL);
1363 // If not try the aux fallback table
1364 if (!sizeof($aPlaceIDs))
1366 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1367 if (sizeof($this->aExcludePlaceIDs))
1369 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1371 //$sSQL .= " limit $this->iLimit";
1372 if (CONST_Debug) var_dump($sSQL);
1373 $aPlaceIDs = $this->oDB->getCol($sSQL);
1376 if (!sizeof($aPlaceIDs))
1378 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1379 if (sizeof($this->aExcludePlaceIDs))
1381 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1383 //$sSQL .= " limit $this->iLimit";
1384 if (CONST_Debug) var_dump($sSQL);
1385 $aPlaceIDs = $this->oDB->getCol($sSQL);
1388 // Fallback to the road
1389 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1391 $aPlaceIDs = $aRoadPlaceIDs;
1396 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1398 $sPlaceIDs = join(',',$aPlaceIDs);
1399 $aClassPlaceIDs = array();
1401 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1403 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1404 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1405 $sSQL .= " and linked_place_id is null";
1406 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1407 $sSQL .= " order by rank_search asc limit $this->iLimit";
1408 if (CONST_Debug) var_dump($sSQL);
1409 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1412 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1414 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1415 $bCacheTable = $this->oDB->getOne($sSQL);
1417 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1419 if (CONST_Debug) var_dump($sSQL);
1420 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1422 // For state / country level searches the normal radius search doesn't work very well
1423 $sPlaceGeom = false;
1424 if ($this->iMaxRank < 9 && $bCacheTable)
1426 // Try and get a polygon to search in instead
1427 $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";
1428 if (CONST_Debug) var_dump($sSQL);
1429 $sPlaceGeom = $this->oDB->getOne($sSQL);
1438 $this->iMaxRank += 5;
1439 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1440 if (CONST_Debug) var_dump($sSQL);
1441 $aPlaceIDs = $this->oDB->getCol($sSQL);
1442 $sPlaceIDs = join(',',$aPlaceIDs);
1445 if ($sPlaceIDs || $sPlaceGeom)
1451 // More efficient - can make the range bigger
1455 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1456 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1457 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1459 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1460 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1463 $sSQL .= ",placex as f where ";
1464 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1469 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1471 if (sizeof($this->aExcludePlaceIDs))
1473 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1475 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1476 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1477 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1478 $sSQL .= " limit $this->iLimit";
1479 if (CONST_Debug) var_dump($sSQL);
1480 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1484 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1487 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1488 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1490 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1491 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1492 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1493 if (sizeof($this->aExcludePlaceIDs))
1495 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1497 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1498 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1499 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1500 $sSQL .= " limit $this->iLimit";
1501 if (CONST_Debug) var_dump($sSQL);
1502 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1507 $aPlaceIDs = $aClassPlaceIDs;
1513 if (PEAR::IsError($aPlaceIDs))
1515 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1518 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1520 foreach($aPlaceIDs as $iPlaceID)
1522 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1524 if ($iQueryLoop > 20) break;
1527 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1529 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1530 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1531 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1532 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1533 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1534 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1535 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1536 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1538 if (CONST_Debug) var_dump($sSQL);
1539 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1543 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1544 if ($iGroupLoop > 4) break;
1545 if ($iQueryLoop > 30) break;
1548 // Did we find anything?
1549 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1551 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1557 // Just interpret as a reverse geocode
1558 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1560 $aSearchResults = $this->getDetails(array($iPlaceID));
1562 $aSearchResults = array();
1566 if (!sizeof($aSearchResults))
1568 if ($this->bFallback)
1570 if ($this->fallbackStructuredQuery())
1572 return $this->lookup();
1579 $aClassType = getClassTypesWithImportance();
1580 $aRecheckWords = preg_split('/\b[\s,\\-]*/u',$sQuery);
1581 foreach($aRecheckWords as $i => $sWord)
1583 if (!$sWord) unset($aRecheckWords[$i]);
1586 foreach($aSearchResults as $iResNum => $aResult)
1588 if (CONST_Search_AreaPolygons)
1590 // Get the bounding box and outline polygon
1591 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1592 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1593 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1594 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1595 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1596 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1597 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1598 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1599 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1600 $aPointPolygon = $this->oDB->getRow($sSQL);
1601 if (PEAR::IsError($aPointPolygon))
1603 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1606 if ($aPointPolygon['place_id'])
1608 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1609 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1610 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1611 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1613 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1615 $aResult['lat'] = $aPointPolygon['centrelat'];
1616 $aResult['lon'] = $aPointPolygon['centrelon'];
1619 if ($this->bIncludePolygonAsPoints)
1621 // Translate geometary string to point array
1622 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1624 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1626 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1628 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1630 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1633 $iSteps = ($fRadius * 40000)^2;
1634 $fStepSize = (2*pi())/$iSteps;
1635 $aPolyPoints = array();
1636 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1638 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1640 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1641 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1642 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1643 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1647 // Output data suitable for display (points and a bounding box)
1648 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1650 $aResult['aPolyPoints'] = array();
1651 foreach($aPolyPoints as $aPoint)
1653 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1656 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1660 if ($aResult['extra_place'] == 'city')
1662 $aResult['class'] = 'place';
1663 $aResult['type'] = 'city';
1664 $aResult['rank_search'] = 16;
1667 if (!isset($aResult['aBoundingBox']))
1670 $fDiameter = 0.0001;
1672 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1673 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1675 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1677 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1678 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1680 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1682 $fRadius = $fDiameter / 2;
1684 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1685 $fStepSize = (2*pi())/$iSteps;
1686 $aPolyPoints = array();
1687 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1689 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1691 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1692 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1693 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1694 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1696 // Output data suitable for display (points and a bounding box)
1697 if ($this->bIncludePolygonAsPoints)
1699 $aResult['aPolyPoints'] = array();
1700 foreach($aPolyPoints as $aPoint)
1702 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1705 $aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
1708 // Is there an icon set for this type of result?
1709 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1710 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1712 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1715 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1716 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1718 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1720 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1721 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1723 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1726 if ($this->bIncludeAddressDetails)
1728 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1729 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1731 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1735 // Adjust importance for the number of exact string matches in the result
1736 $aResult['importance'] = max(0.001,$aResult['importance']);
1738 $sAddress = $aResult['langaddress'];
1739 foreach($aRecheckWords as $i => $sWord)
1741 if (stripos($sAddress, $sWord)!==false)
1744 if (preg_match("/(^|,)\s*$sWord\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1748 $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
1750 $aResult['name'] = $aResult['langaddress'];
1751 // secondary ordering (for results with same importance (the smaller the better):
1752 // - approximate importance of address parts
1753 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1754 // - number of exact matches from the query
1755 if (isset($this->exactMatchCache[$aResult['place_id']]))
1756 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1757 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1758 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1759 // - importance of the class/type
1760 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1761 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1763 $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1767 $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1769 $aSearchResults[$iResNum] = $aResult;
1771 uasort($aSearchResults, 'byImportance');
1773 $aOSMIDDone = array();
1774 $aClassTypeNameDone = array();
1775 $aToFilter = $aSearchResults;
1776 $aSearchResults = array();
1779 foreach($aToFilter as $iResNum => $aResult)
1781 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1782 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1785 $fLat = $aResult['lat'];
1786 $fLon = $aResult['lon'];
1787 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1790 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1791 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1793 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1794 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1795 $aSearchResults[] = $aResult;
1798 // Absolute limit on number of results
1799 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1802 return $aSearchResults;