6 protected $aLangPrefOrder = array();
8 protected $bIncludeAddressDetails = false;
9 protected $bIncludeExtraTags = false;
10 protected $bIncludeNameDetails = false;
12 protected $bIncludePolygonAsPoints = false;
13 protected $bIncludePolygonAsText = false;
14 protected $bIncludePolygonAsGeoJSON = false;
15 protected $bIncludePolygonAsKML = false;
16 protected $bIncludePolygonAsSVG = false;
17 protected $fPolygonSimplificationThreshold = 0.0;
19 protected $aExcludePlaceIDs = array();
20 protected $bDeDupe = true;
21 protected $bReverseInPlan = false;
23 protected $iLimit = 20;
24 protected $iFinalLimit = 10;
25 protected $iOffset = 0;
26 protected $bFallback = false;
28 protected $aCountryCodes = false;
29 protected $aNearPoint = false;
31 protected $bBoundedSearch = false;
32 protected $aViewBox = false;
33 protected $sViewboxSmallSQL = false;
34 protected $sViewboxLargeSQL = false;
35 protected $aRoutePoints = false;
37 protected $iMaxRank = 20;
38 protected $iMinAddressRank = 0;
39 protected $iMaxAddressRank = 30;
40 protected $aAddressRankList = array();
41 protected $exactMatchCache = array();
43 protected $sAllowedTypesSQLList = false;
45 protected $sQuery = false;
46 protected $aStructuredQuery = false;
48 //for Tiger housenumber interpolation
49 protected $searchedHousenumber=-1;
50 protected $housenumberFound=false;
52 function Geocode(&$oDB)
57 function setReverseInPlan($bReverse)
59 $this->bReverseInPlan = $bReverse;
62 function setLanguagePreference($aLangPref)
64 $this->aLangPrefOrder = $aLangPref;
67 function setIncludeAddressDetails($bAddressDetails = true)
69 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
72 function getIncludeAddressDetails()
74 return $this->bIncludeAddressDetails;
77 function getIncludeExtraTags()
79 return $this->bIncludeExtraTags;
82 function getIncludeNameDetails()
84 return $this->bIncludeNameDetails;
87 function setIncludePolygonAsPoints($b = true)
89 $this->bIncludePolygonAsPoints = $b;
92 function getIncludePolygonAsPoints()
94 return $this->bIncludePolygonAsPoints;
97 function setIncludePolygonAsText($b = true)
99 $this->bIncludePolygonAsText = $b;
102 function getIncludePolygonAsText()
104 return $this->bIncludePolygonAsText;
107 function setIncludePolygonAsGeoJSON($b = true)
109 $this->bIncludePolygonAsGeoJSON = $b;
112 function setIncludePolygonAsKML($b = true)
114 $this->bIncludePolygonAsKML = $b;
117 function setIncludePolygonAsSVG($b = true)
119 $this->bIncludePolygonAsSVG = $b;
122 function setPolygonSimplificationThreshold($f)
124 $this->fPolygonSimplificationThreshold = $f;
127 function setDeDupe($bDeDupe = true)
129 $this->bDeDupe = (bool)$bDeDupe;
132 function setLimit($iLimit = 10)
134 if ($iLimit > 50) $iLimit = 50;
135 if ($iLimit < 1) $iLimit = 1;
137 $this->iFinalLimit = $iLimit;
138 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
141 function setOffset($iOffset = 0)
143 $this->iOffset = $iOffset;
146 function setFallback($bFallback = true)
148 $this->bFallback = (bool)$bFallback;
151 function setExcludedPlaceIDs($a)
153 // TODO: force to int
154 $this->aExcludePlaceIDs = $a;
157 function getExcludedPlaceIDs()
159 return $this->aExcludePlaceIDs;
162 function setBounded($bBoundedSearch = true)
164 $this->bBoundedSearch = (bool)$bBoundedSearch;
167 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
169 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
172 function getViewBoxString()
174 if (!$this->aViewBox) return null;
175 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
178 function setRoute($aRoutePoints)
180 $this->aRoutePoints = $aRoutePoints;
183 function setFeatureType($sFeatureType)
185 switch($sFeatureType)
188 $this->setRankRange(4, 4);
191 $this->setRankRange(8, 8);
194 $this->setRankRange(14, 16);
197 $this->setRankRange(8, 20);
202 function setRankRange($iMin, $iMax)
204 $this->iMinAddressRank = (int)$iMin;
205 $this->iMaxAddressRank = (int)$iMax;
208 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
210 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
213 function setCountryCodesList($aCountryCodes)
215 $this->aCountryCodes = $aCountryCodes;
218 function setQuery($sQueryString)
220 $this->sQuery = $sQueryString;
221 $this->aStructuredQuery = false;
224 function getQueryString()
226 return $this->sQuery;
230 function loadParamArray($aParams)
232 if (isset($aParams['addressdetails'])) $this->bIncludeAddressDetails = (bool)$aParams['addressdetails'];
233 if ((float) CONST_Postgresql_Version > 9.2)
235 if (isset($aParams['extratags'])) $this->bIncludeExtraTags = (bool)$aParams['extratags'];
236 if (isset($aParams['namedetails'])) $this->bIncludeNameDetails = (bool)$aParams['namedetails'];
238 if (isset($aParams['bounded'])) $this->bBoundedSearch = (bool)$aParams['bounded'];
239 if (isset($aParams['dedupe'])) $this->bDeDupe = (bool)$aParams['dedupe'];
241 if (isset($aParams['limit'])) $this->setLimit((int)$aParams['limit']);
242 if (isset($aParams['offset'])) $this->iOffset = (int)$aParams['offset'];
244 if (isset($aParams['fallback'])) $this->bFallback = (bool)$aParams['fallback'];
246 // List of excluded Place IDs - used for more acurate pageing
247 if (isset($aParams['exclude_place_ids']) && $aParams['exclude_place_ids'])
249 foreach(explode(',',$aParams['exclude_place_ids']) as $iExcludedPlaceID)
251 $iExcludedPlaceID = (int)$iExcludedPlaceID;
252 if ($iExcludedPlaceID)
253 $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
256 if (isset($aExcludePlaceIDs))
257 $this->aExcludePlaceIDs = $aExcludePlaceIDs;
260 // Only certain ranks of feature
261 if (isset($aParams['featureType'])) $this->setFeatureType($aParams['featureType']);
262 if (isset($aParams['featuretype'])) $this->setFeatureType($aParams['featuretype']);
265 if (isset($aParams['countrycodes']))
267 $aCountryCodes = array();
268 foreach(explode(',',$aParams['countrycodes']) as $sCountryCode)
270 if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
272 $aCountryCodes[] = strtolower($sCountryCode);
275 $this->aCountryCodes = $aCountryCodes;
278 if (isset($aParams['viewboxlbrt']) && $aParams['viewboxlbrt'])
280 $aCoOrdinatesLBRT = explode(',',$aParams['viewboxlbrt']);
281 $this->setViewBox($aCoOrdinatesLBRT[0], $aCoOrdinatesLBRT[1], $aCoOrdinatesLBRT[2], $aCoOrdinatesLBRT[3]);
283 else if (isset($aParams['viewbox']) && $aParams['viewbox'])
285 $aCoOrdinatesLTRB = explode(',',$aParams['viewbox']);
286 $this->setViewBox($aCoOrdinatesLTRB[0], $aCoOrdinatesLTRB[3], $aCoOrdinatesLTRB[2], $aCoOrdinatesLTRB[1]);
289 if (isset($aParams['route']) && $aParams['route'] && isset($aParams['routewidth']) && $aParams['routewidth'])
291 $aPoints = explode(',',$aParams['route']);
292 if (sizeof($aPoints) % 2 != 0)
294 userError("Uneven number of points");
299 foreach($aPoints as $i => $fPoint)
303 $aRoute[] = array((float)$fPoint, $fPrevCoord);
307 $fPrevCoord = (float)$fPoint;
310 $this->aRoutePoints = $aRoute;
314 function setQueryFromParams($aParams)
317 $sQuery = (isset($aParams['q'])?trim($aParams['q']):'');
320 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
321 $this->setReverseInPlan(false);
325 $this->setQuery($sQuery);
329 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
331 $sValue = trim($sValue);
332 if (!$sValue) return false;
333 $this->aStructuredQuery[$sKey] = $sValue;
334 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
336 $this->iMinAddressRank = $iNewMinAddressRank;
337 $this->iMaxAddressRank = $iNewMaxAddressRank;
339 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
343 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
345 $this->sQuery = false;
348 $this->iMinAddressRank = 0;
349 $this->iMaxAddressRank = 30;
350 $this->aAddressRankList = array();
352 $this->aStructuredQuery = array();
353 $this->sAllowedTypesSQLList = '';
355 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
356 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
357 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
358 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
359 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
360 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
361 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
363 if (sizeof($this->aStructuredQuery) > 0)
365 $this->sQuery = join(', ', $this->aStructuredQuery);
366 if ($this->iMaxAddressRank < 30)
368 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
373 function fallbackStructuredQuery()
375 if (!$this->aStructuredQuery) return false;
377 $aParams = $this->aStructuredQuery;
379 if (sizeof($aParams) == 1) return false;
381 $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
383 foreach($aOrderToFallback as $sType)
385 if (isset($aParams[$sType]))
387 unset($aParams[$sType]);
388 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
396 function getDetails($aPlaceIDs)
398 if (sizeof($aPlaceIDs) == 0) return array();
400 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
402 // Get the details for display (is this a redundant extra step?)
403 $sPlaceIDs = join(',',$aPlaceIDs);
405 $sImportanceSQL = '';
406 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
407 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
409 $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,";
410 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
411 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
412 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
413 if ($this->bIncludeExtraTags) $sSQL .= "hstore_to_json(extratags)::text as extra,";
414 if ($this->bIncludeNameDetails) $sSQL .= "hstore_to_json(name)::text as names,";
415 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
416 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
417 $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, ";
418 $sSQL .= "(extratags->'place') as extra_place ";
419 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
420 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
421 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
422 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
424 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
425 $sSQL .= "and linked_place_id is null ";
426 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
427 if (!$this->bDeDupe) $sSQL .= ",place_id";
428 $sSQL .= ",langaddress ";
429 $sSQL .= ",placename ";
431 if ($this->bIncludeExtraTags) $sSQL .= ",extratags";
432 if ($this->bIncludeNameDetails) $sSQL .= ",name";
433 $sSQL .= ",extratags->'place' ";
435 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
437 //query also location_property_tiger_line and location_property_aux
438 //Tiger search only if it was searched for a housenumber (searchedHousenumber >=0) and if it was found (housenumberFound = true)
439 //only Tiger housenumbers need to be interpolated, because they are saved as lines with start- and endnumber, the common osm housenumbers are usually saved as points
440 if($this->searchedHousenumber>=0 && $this->housenumberFound){
442 $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";
443 $sSQL .= ", get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress ";
444 $sSQL .= ", null as placename";
445 $sSQL .= ", null as ref";
446 if ($this->bIncludeExtraTags) $sSQL .= ", null as extra";
447 if ($this->bIncludeNameDetails) $sSQL .= ", null as names";
448 $sSQL .= ", avg(st_x(point)) as lon, avg(st_y(point)) as lat";
449 $sSQL .= $sImportanceSQL.", -1.15 as importance ";
450 $sSQL .= ", 1.0 as addressimportance "; //not sure how the addressimportance is/should be calculated for Tiger data
451 $sSQL .= ", null as extra_place ";
452 $sSQL .= " from (select place_id";
453 //interpolate the Tiger housenumbers here
454 $sSQL .= ",ST_LineInterpolatePoint(linegeo, ($this->searchedHousenumber::float-startnumber::float)/(endnumber-startnumber)::float) as point, parent_place_id ";
455 $sSQL .= "from location_property_tiger_line where place_id in ($sPlaceIDs) ";
456 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank) as blub"; //postgres wants an alias here
457 $sSQL .= " group by place_id"; //why group by place_id, isnt place_id unique?
458 if (!$this->bDeDupe) $sSQL .= ",place_id ";
461 $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,";
462 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
463 $sSQL .= "null as placename,";
464 $sSQL .= "null as ref,";
465 if ($this->bIncludeExtraTags) $sSQL .= "null as extra,";
466 if ($this->bIncludeNameDetails) $sSQL .= "null as names,";
467 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
468 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
469 $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, ";
470 $sSQL .= "null as extra_place ";
471 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
472 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
473 $sSQL .= "group by place_id";
474 if (!$this->bDeDupe) $sSQL .= ",place_id";
475 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
478 $sSQL .= " order by importance desc";
479 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
480 $aSearchResults = $this->oDB->getAll($sSQL);
482 if (PEAR::IsError($aSearchResults))
484 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
487 return $aSearchResults;
490 function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
493 Calculate all searches using aValidTokens i.e.
494 'Wodsworth Road, Sheffield' =>
498 0 1 (wodsworth)(road)
501 Score how good the search is so they can be ordered
503 foreach($aPhrases as $iPhrase => $sPhrase)
505 $aNewPhraseSearches = array();
506 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
507 else $sPhraseType = '';
509 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
511 // Too many permutations - too expensive
512 if ($iWordSet > 120) break;
514 $aWordsetSearches = $aSearches;
516 // Add all words from this wordset
517 foreach($aWordset as $iToken => $sToken)
519 //echo "<br><b>$sToken</b>";
520 $aNewWordsetSearches = array();
522 foreach($aWordsetSearches as $aCurrentSearch)
525 //var_dump($aCurrentSearch);
528 // If the token is valid
529 if (isset($aValidTokens[' '.$sToken]))
531 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
533 $aSearch = $aCurrentSearch;
534 $aSearch['iSearchRank']++;
535 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
537 if ($aSearch['sCountryCode'] === false)
539 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
540 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
541 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)))
543 $aSearch['iSearchRank'] += 5;
545 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
548 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
550 if ($aSearch['fLat'] === '')
552 $aSearch['fLat'] = $aSearchTerm['lat'];
553 $aSearch['fLon'] = $aSearchTerm['lon'];
554 $aSearch['fRadius'] = $aSearchTerm['radius'];
555 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
558 elseif ($sPhraseType == 'postalcode')
560 // 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
561 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
563 // If we already have a name try putting the postcode first
564 if (sizeof($aSearch['aName']))
566 $aNewSearch = $aSearch;
567 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
568 $aNewSearch['aName'] = array();
569 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
570 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
573 if (sizeof($aSearch['aName']))
575 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
577 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
581 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
582 $aSearch['iSearchRank'] += 1000; // skip;
587 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
588 //$aSearch['iNamePhrase'] = $iPhrase;
590 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
594 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
596 if ($aSearch['sHouseNumber'] === '')
598 $aSearch['sHouseNumber'] = $sToken;
599 // sanity check: if the housenumber is not mainly made
600 // up of numbers, add a penalty
601 if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
602 // also housenumbers should appear in the first or second phrase
603 if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
604 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
606 // Fall back to not searching for this item (better than nothing)
607 $aSearch = $aCurrentSearch;
608 $aSearch['iSearchRank'] += 1;
609 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
613 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
615 if ($aSearch['sClass'] === '')
617 $aSearch['sOperator'] = $aSearchTerm['operator'];
618 $aSearch['sClass'] = $aSearchTerm['class'];
619 $aSearch['sType'] = $aSearchTerm['type'];
620 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
621 else $aSearch['sOperator'] = 'near'; // near = in for the moment
622 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
624 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
627 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
629 if (sizeof($aSearch['aName']))
631 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
633 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
637 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
638 $aSearch['iSearchRank'] += 1000; // skip;
643 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
644 //$aSearch['iNamePhrase'] = $iPhrase;
646 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
650 // Look for partial matches.
651 // Note that there is no point in adding country terms here
652 // because country are omitted in the address.
653 if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country')
655 // Allow searching for a word - but at extra cost
656 foreach($aValidTokens[$sToken] as $aSearchTerm)
658 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
660 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false)
662 $aSearch = $aCurrentSearch;
663 $aSearch['iSearchRank'] += 1;
664 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
666 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
667 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
669 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
671 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
672 $aSearch['iSearchRank'] += 1;
673 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
674 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
676 if (empty($aSearchTermToken['country_code'])
677 && empty($aSearchTermToken['lat'])
678 && empty($aSearchTermToken['class']))
680 $aSearch = $aCurrentSearch;
681 $aSearch['iSearchRank'] += 1;
682 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
683 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
689 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
690 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
691 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
695 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
697 $aSearch = $aCurrentSearch;
698 $aSearch['iSearchRank'] += 1;
699 if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
700 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
701 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
702 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
704 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
705 $aSearch['iNamePhrase'] = $iPhrase;
706 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
713 // Allow skipping a word - but at EXTREAM cost
714 //$aSearch = $aCurrentSearch;
715 //$aSearch['iSearchRank']+=100;
716 //$aNewWordsetSearches[] = $aSearch;
720 usort($aNewWordsetSearches, 'bySearchRank');
721 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
723 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
725 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
726 usort($aNewPhraseSearches, 'bySearchRank');
728 $aSearchHash = array();
729 foreach($aNewPhraseSearches as $iSearch => $aSearch)
731 $sHash = serialize($aSearch);
732 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
733 else $aSearchHash[$sHash] = 1;
736 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
739 // Re-group the searches by their score, junk anything over 20 as just not worth trying
740 $aGroupedSearches = array();
741 foreach($aNewPhraseSearches as $aSearch)
743 if ($aSearch['iSearchRank'] < $this->iMaxRank)
745 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
746 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
749 ksort($aGroupedSearches);
752 $aSearches = array();
753 foreach($aGroupedSearches as $iScore => $aNewSearches)
755 $iSearchCount += sizeof($aNewSearches);
756 $aSearches = array_merge($aSearches, $aNewSearches);
757 if ($iSearchCount > 50) break;
760 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
763 return $aGroupedSearches;
767 /* Perform the actual query lookup.
769 Returns an ordered list of results, each with the following fields:
770 osm_type: type of corresponding OSM object
774 P - postcode (internally computed)
775 osm_id: id of corresponding OSM object
776 class: general object class (corresponds to tag key of primary OSM tag)
777 type: subclass of object (corresponds to tag value of primary OSM tag)
778 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
779 rank_search: rank in search hierarchy
780 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
781 rank_address: rank in address hierarchy (determines orer in address)
782 place_id: internal key (may differ between different instances)
783 country_code: ISO country code
784 langaddress: localized full address
785 placename: localized name of object
786 ref: content of ref tag (if available)
789 importance: importance of place based on Wikipedia link count
790 addressimportance: cumulated importance of address elements
791 extra_place: type of place (for admin boundaries, if there is a place tag)
792 aBoundingBox: bounding Box
793 label: short description of the object class/type (English only)
794 name: full name (currently the same as langaddress)
795 foundorder: secondary ordering for places with same importance
799 if (!$this->sQuery && !$this->aStructuredQuery) return false;
801 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
802 $sCountryCodesSQL = false;
803 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
805 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
808 $sQuery = $this->sQuery;
810 // Conflicts between US state abreviations and various words for 'the' in different languages
811 if (isset($this->aLangPrefOrder['name:en']))
813 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/','\1illinois\2', $sQuery);
814 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/','\1alabama\2', $sQuery);
815 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/','\1louisiana\2', $sQuery);
819 $sViewboxCentreSQL = false;
820 $bBoundingBoxSearch = false;
823 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
824 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
825 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
826 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
827 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
828 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
830 $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)";
831 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
832 $bBoundingBoxSearch = $this->bBoundedSearch;
836 if ($this->aRoutePoints)
838 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
840 foreach($this->aRoutePoints as $aPoint)
842 if (!$bFirst) $sViewboxCentreSQL .= ",";
843 $sViewboxCentreSQL .= $aPoint[0].' '.$aPoint[1];
846 $sViewboxCentreSQL .= ")'::geometry,4326)";
848 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
849 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
850 if (PEAR::isError($this->sViewboxSmallSQL))
852 failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
854 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
856 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
857 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
858 if (PEAR::isError($this->sViewboxLargeSQL))
860 failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
862 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
863 $bBoundingBoxSearch = $this->bBoundedSearch;
866 // Do we have anything that looks like a lat/lon pair?
867 if ( $aLooksLike = looksLikeLatLonPair($sQuery) ){
868 $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
869 $sQuery = $aLooksLike['query'];
872 $aSearchResults = array();
873 if ($sQuery || $this->aStructuredQuery)
875 // Start with a blank search
877 array('iSearchRank' => 0,
879 'sCountryCode' => false,
881 'aAddress' => array(),
882 'aFullNameAddress' => array(),
883 'aNameNonSearch' => array(),
884 'aAddressNonSearch' => array(),
886 'aFeatureName' => array(),
889 'sHouseNumber' => '',
896 // Do we have a radius search?
897 $sNearPointSQL = false;
898 if ($this->aNearPoint)
900 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
901 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
902 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
903 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
906 // Any 'special' terms in the search?
907 $bSpecialTerms = false;
908 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
909 $aSpecialTerms = array();
910 foreach($aSpecialTermsRaw as $aSpecialTerm)
912 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
913 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
916 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
917 $aSpecialTerms = array();
918 if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity'])
920 $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
921 unset($this->aStructuredQuery['amenity']);
923 foreach($aSpecialTermsRaw as $aSpecialTerm)
925 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
926 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
927 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
928 $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';
929 if (CONST_Debug) var_Dump($sSQL);
930 $aSearchWords = $this->oDB->getAll($sSQL);
931 $aNewSearches = array();
932 foreach($aSearches as $aSearch)
934 foreach($aSearchWords as $aSearchTerm)
936 $aNewSearch = $aSearch;
937 if ($aSearchTerm['country_code'])
939 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
940 $aNewSearches[] = $aNewSearch;
941 $bSpecialTerms = true;
943 if ($aSearchTerm['class'])
945 $aNewSearch['sClass'] = $aSearchTerm['class'];
946 $aNewSearch['sType'] = $aSearchTerm['type'];
947 $aNewSearches[] = $aNewSearch;
948 $bSpecialTerms = true;
952 $aSearches = $aNewSearches;
955 // Split query into phrases
956 // Commas are used to reduce the search space by indicating where phrases split
957 if ($this->aStructuredQuery)
959 $aPhrases = $this->aStructuredQuery;
960 $bStructuredPhrases = true;
964 $aPhrases = explode(',',$sQuery);
965 $bStructuredPhrases = false;
968 // Convert each phrase to standard form
969 // Create a list of standard words
970 // Get all 'sets' of words
971 // Generate a complete list of all
973 foreach($aPhrases as $iPhrase => $sPhrase)
975 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
976 if (PEAR::isError($aPhrase))
978 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
979 if (CONST_Debug) var_dump($aPhrase);
982 if (trim($aPhrase['string']))
984 $aPhrases[$iPhrase] = $aPhrase;
985 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
986 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
987 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
991 unset($aPhrases[$iPhrase]);
995 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
996 $aPhraseTypes = array_keys($aPhrases);
997 $aPhrases = array_values($aPhrases);
999 if (sizeof($aTokens))
1001 // Check which tokens we have, get the ID numbers
1002 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
1003 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
1005 if (CONST_Debug) var_Dump($sSQL);
1007 $aValidTokens = array();
1008 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
1009 else $aDatabaseWords = array();
1010 if (PEAR::IsError($aDatabaseWords))
1012 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
1014 $aPossibleMainWordIDs = array();
1015 $aWordFrequencyScores = array();
1016 foreach($aDatabaseWords as $aToken)
1018 // Very special case - require 2 letter country param to match the country code found
1019 if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
1020 && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code'])
1025 if (isset($aValidTokens[$aToken['word_token']]))
1027 $aValidTokens[$aToken['word_token']][] = $aToken;
1031 $aValidTokens[$aToken['word_token']] = array($aToken);
1033 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
1034 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
1036 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
1038 // Try and calculate GB postcodes we might be missing
1039 foreach($aTokens as $sToken)
1041 // Source of gb postcodes is now definitive - always use
1042 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
1044 if (substr($aData[1],-2,1) != ' ')
1046 $aData[0] = substr($aData[0],0,strlen($aData[1])-1).' '.substr($aData[0],strlen($aData[1])-1);
1047 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
1049 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
1050 if ($aGBPostcodeLocation)
1052 $aValidTokens[$sToken] = $aGBPostcodeLocation;
1055 // US ZIP+4 codes - if there is no token,
1056 // merge in the 5-digit ZIP code
1057 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
1059 if (isset($aValidTokens[$aData[1]]))
1061 foreach($aValidTokens[$aData[1]] as $aToken)
1063 if (!$aToken['class'])
1065 if (isset($aValidTokens[$sToken]))
1067 $aValidTokens[$sToken][] = $aToken;
1071 $aValidTokens[$sToken] = array($aToken);
1079 foreach($aTokens as $sToken)
1081 // Unknown single word token with a number - assume it is a house number
1082 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
1084 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
1088 // Any words that have failed completely?
1089 // TODO: suggestions
1091 // Start the search process
1092 $aResultPlaceIDs = array();
1094 $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1096 if ($this->bReverseInPlan)
1098 // Reverse phrase array and also reverse the order of the wordsets in
1099 // the first and final phrase. Don't bother about phrases in the middle
1100 // because order in the address doesn't matter.
1101 $aPhrases = array_reverse($aPhrases);
1102 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1103 if (sizeof($aPhrases) > 1)
1105 $aFinalPhrase = end($aPhrases);
1106 $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1108 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1110 foreach($aGroupedSearches as $aSearches)
1112 foreach($aSearches as $aSearch)
1114 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1116 if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1117 $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1123 $aGroupedSearches = $aReverseGroupedSearches;
1124 ksort($aGroupedSearches);
1129 // Re-group the searches by their score, junk anything over 20 as just not worth trying
1130 $aGroupedSearches = array();
1131 foreach($aSearches as $aSearch)
1133 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1135 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1136 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1139 ksort($aGroupedSearches);
1142 if (CONST_Debug) var_Dump($aGroupedSearches);
1144 if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0)
1146 $aCopyGroupedSearches = $aGroupedSearches;
1147 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1149 foreach($aSearches as $iSearch => $aSearch)
1151 $aReductionsList = array($aSearch['aAddress']);
1152 $iSearchRank = $aSearch['iSearchRank'];
1153 while(sizeof($aReductionsList) > 0)
1156 if ($iSearchRank > iMaxRank) break 3;
1157 $aNewReductionsList = array();
1158 foreach($aReductionsList as $aReductionsWordList)
1160 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1162 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1163 $aReverseSearch = $aSearch;
1164 $aSearch['aAddress'] = $aReductionsWordListResult;
1165 $aSearch['iSearchRank'] = $iSearchRank;
1166 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1167 if (sizeof($aReductionsWordListResult) > 0)
1169 $aNewReductionsList[] = $aReductionsWordListResult;
1173 $aReductionsList = $aNewReductionsList;
1177 ksort($aGroupedSearches);
1180 // Filter out duplicate searches
1181 $aSearchHash = array();
1182 foreach($aGroupedSearches as $iGroup => $aSearches)
1184 foreach($aSearches as $iSearch => $aSearch)
1186 $sHash = serialize($aSearch);
1187 if (isset($aSearchHash[$sHash]))
1189 unset($aGroupedSearches[$iGroup][$iSearch]);
1190 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1194 $aSearchHash[$sHash] = 1;
1199 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1203 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1206 foreach($aSearches as $aSearch)
1210 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1211 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1213 // No location term?
1214 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1216 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1218 // Just looking for a country by code - look it up
1219 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1221 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1222 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1223 if ($bBoundingBoxSearch)
1224 $sSQL .= " and _st_intersects($this->sViewboxSmallSQL, geometry)";
1225 $sSQL .= " order by st_area(geometry) desc limit 1";
1226 if (CONST_Debug) var_dump($sSQL);
1227 $aPlaceIDs = $this->oDB->getCol($sSQL);
1231 $aPlaceIDs = array();
1236 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1237 if (!$aSearch['sClass']) continue;
1238 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1239 if ($this->oDB->getOne($sSQL))
1241 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1242 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1243 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1244 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1245 if (sizeof($this->aExcludePlaceIDs))
1247 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1249 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1250 $sSQL .= " limit $this->iLimit";
1251 if (CONST_Debug) var_dump($sSQL);
1252 $aPlaceIDs = $this->oDB->getCol($sSQL);
1254 // If excluded place IDs are given, it is fair to assume that
1255 // there have been results in the small box, so no further
1256 // expansion in that case.
1257 // Also don't expand if bounded results were requested.
1258 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch)
1260 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1261 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1262 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1263 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1264 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1265 $sSQL .= " limit $this->iLimit";
1266 if (CONST_Debug) var_dump($sSQL);
1267 $aPlaceIDs = $this->oDB->getCol($sSQL);
1272 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1273 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1274 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1275 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1276 $sSQL .= " limit $this->iLimit";
1277 if (CONST_Debug) var_dump($sSQL);
1278 $aPlaceIDs = $this->oDB->getCol($sSQL);
1284 $aPlaceIDs = array();
1286 // First we need a position, either aName or fLat or both
1290 if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress']))
1292 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1293 $aOrder[] = "exists(select place_id from placex where parent_place_id = search_name.place_id and transliteration(housenumber) ~* E'".$sHouseNumberRegex."' limit 1) desc";
1296 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1297 // they might be right - but they are just too darned expensive to run
1298 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1299 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1300 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1302 // For infrequent name terms disable index usage for address
1303 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1304 sizeof($aSearch['aName']) == 1 &&
1305 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1307 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1311 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1312 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1315 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1316 if ($aSearch['sHouseNumber'])
1318 $aTerms[] = "address_rank between 16 and 27";
1322 if ($this->iMinAddressRank > 0)
1324 $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1326 if ($this->iMaxAddressRank < 30)
1328 $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1331 if ($aSearch['fLon'] && $aSearch['fLat'])
1333 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1334 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1336 if (sizeof($this->aExcludePlaceIDs))
1338 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1340 if ($sCountryCodesSQL)
1342 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1345 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1346 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1348 if ($aSearch['sHouseNumber'])
1350 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1354 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1356 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1357 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1359 $aOrder[] = "$sImportanceSQL DESC";
1360 if (sizeof($aSearch['aFullNameAddress']))
1362 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1363 $aOrder[] = 'exactmatch DESC';
1365 $sExactMatchSQL = '0::int as exactmatch';
1368 if (sizeof($aTerms))
1370 $sSQL = "select place_id, ";
1371 $sSQL .= $sExactMatchSQL;
1372 $sSQL .= " from search_name";
1373 $sSQL .= " where ".join(' and ',$aTerms);
1374 $sSQL .= " order by ".join(', ',$aOrder);
1375 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1376 $sSQL .= " limit 20";
1377 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1378 $sSQL .= " limit 1";
1380 $sSQL .= " limit ".$this->iLimit;
1382 if (CONST_Debug) { var_dump($sSQL); }
1383 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1384 if (PEAR::IsError($aViewBoxPlaceIDs))
1386 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1388 //var_dump($aViewBoxPlaceIDs);
1389 // Did we have an viewbox matches?
1390 $aPlaceIDs = array();
1391 $bViewBoxMatch = false;
1392 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1394 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1395 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1396 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1397 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1398 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1399 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1402 //var_Dump($aPlaceIDs);
1405 //now search for housenumber, if housenumber provided
1406 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1408 $aRoadPlaceIDs = $aPlaceIDs;
1409 $sPlaceIDs = join(',',$aPlaceIDs);
1411 // Now they are indexed look for a house attached to a street we found
1412 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1413 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1414 if (sizeof($this->aExcludePlaceIDs))
1416 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1418 $sSQL .= " limit $this->iLimit";
1419 if (CONST_Debug) var_dump($sSQL);
1420 $aPlaceIDs = $this->oDB->getCol($sSQL);
1422 // If nothing found try the aux fallback table
1423 if (!sizeof($aPlaceIDs))
1425 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1426 if (sizeof($this->aExcludePlaceIDs))
1428 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1430 //$sSQL .= " limit $this->iLimit";
1431 if (CONST_Debug) var_dump($sSQL);
1432 $aPlaceIDs = $this->oDB->getCol($sSQL);
1434 //if nothing was found in placex or location_property_aux, then search in Tiger data for this housenumber(location_property_tiger_line)
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 //new query for lines, not housenumbers anymore
1439 $this->searchedHousenumber = intval($aSearch['sHouseNumber']);
1440 if($this->searchedHousenumber%2==0){
1441 //if housenumber is even, look for housenumber in streets with interpolationtype even or all
1442 $sSQL = "select distinct place_id from location_property_tiger_line where parent_place_id in (".$sPlaceIDs.") and (interpolationtype='even' or interpolationtype='all') and ".$this->searchedHousenumber.">=startnumber and ".$this->searchedHousenumber."<=endnumber";
1444 //look for housenumber in streets with interpolationtype odd or all
1445 $sSQL = "select distinct place_id from location_property_tiger_line where parent_place_id in (".$sPlaceIDs.") and (interpolationtype='odd' or interpolationtype='all') and ".$this->searchedHousenumber.">=startnumber and ".$this->searchedHousenumber."<=endnumber";
1448 if (sizeof($this->aExcludePlaceIDs))
1450 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1452 //$sSQL .= " limit $this->iLimit";
1453 if (CONST_Debug) var_dump($sSQL);
1455 $aPlaceIDs = $this->oDB->getCol($sSQL,0);
1458 // Fallback to the road (if no housenumber was found)
1459 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1461 $aPlaceIDs = $aRoadPlaceIDs;
1462 //set to false, if no housenumbers were found
1463 $this->housenumberFound=false;
1465 //housenumber was found
1466 $this->housenumberFound=true;
1471 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1473 $sPlaceIDs = join(',',$aPlaceIDs);
1474 $aClassPlaceIDs = array();
1476 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1478 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1479 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1480 $sSQL .= " and linked_place_id is null";
1481 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1482 $sSQL .= " order by rank_search asc limit $this->iLimit";
1483 if (CONST_Debug) var_dump($sSQL);
1484 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1487 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1489 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1490 $bCacheTable = $this->oDB->getOne($sSQL);
1492 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1494 if (CONST_Debug) var_dump($sSQL);
1495 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1497 // For state / country level searches the normal radius search doesn't work very well
1498 $sPlaceGeom = false;
1499 if ($this->iMaxRank < 9 && $bCacheTable)
1501 // Try and get a polygon to search in instead
1502 $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";
1503 if (CONST_Debug) var_dump($sSQL);
1504 $sPlaceGeom = $this->oDB->getOne($sSQL);
1513 $this->iMaxRank += 5;
1514 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1515 if (CONST_Debug) var_dump($sSQL);
1516 $aPlaceIDs = $this->oDB->getCol($sSQL);
1517 $sPlaceIDs = join(',',$aPlaceIDs);
1520 if ($sPlaceIDs || $sPlaceGeom)
1526 // More efficient - can make the range bigger
1530 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1531 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1532 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1534 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1535 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1538 $sSQL .= ",placex as f where ";
1539 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1544 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1546 if (sizeof($this->aExcludePlaceIDs))
1548 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1550 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1551 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1552 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1553 $sSQL .= " limit $this->iLimit";
1554 if (CONST_Debug) var_dump($sSQL);
1555 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1559 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1562 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1563 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1565 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1566 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1567 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1568 if (sizeof($this->aExcludePlaceIDs))
1570 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1572 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1573 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1574 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1575 $sSQL .= " limit $this->iLimit";
1576 if (CONST_Debug) var_dump($sSQL);
1577 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1582 $aPlaceIDs = $aClassPlaceIDs;
1588 if (PEAR::IsError($aPlaceIDs))
1590 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1593 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1595 foreach($aPlaceIDs as $iPlaceID)
1597 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1599 if ($iQueryLoop > 20) break;
1602 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1604 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1605 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1606 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1607 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1608 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1609 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1610 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1611 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1613 if (CONST_Debug) var_dump($sSQL);
1614 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1618 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1619 if ($iGroupLoop > 4) break;
1620 if ($iQueryLoop > 30) break;
1623 // Did we find anything?
1624 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1626 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1632 // Just interpret as a reverse geocode
1633 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1635 $aSearchResults = $this->getDetails(array($iPlaceID));
1637 $aSearchResults = array();
1641 if (!sizeof($aSearchResults))
1643 if ($this->bFallback)
1645 if ($this->fallbackStructuredQuery())
1647 return $this->lookup();
1654 $aClassType = getClassTypesWithImportance();
1655 $aRecheckWords = preg_split('/\b[\s,\\-]*/u',$sQuery);
1656 foreach($aRecheckWords as $i => $sWord)
1658 if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1661 if (CONST_Debug) { echo '<i>Recheck words:<\i>'; var_dump($aRecheckWords); }
1663 foreach($aSearchResults as $iResNum => $aResult)
1666 $fDiameter = 0.0001;
1668 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1669 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1671 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'];
1673 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1674 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1676 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1678 $fRadius = $fDiameter / 2;
1680 if (CONST_Search_AreaPolygons)
1682 // Get the bounding box and outline polygon
1683 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1684 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1685 $sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,";
1686 $sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon";
1687 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1688 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1689 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1690 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1691 $sFrom = " from placex where place_id = ".$aResult['place_id'];
1692 if ($this->fPolygonSimplificationThreshold > 0)
1694 $sSQL .= " from (select place_id,centroid,ST_SimplifyPreserveTopology(geometry,".$this->fPolygonSimplificationThreshold.") as geometry".$sFrom.") as plx";
1701 $aPointPolygon = $this->oDB->getRow($sSQL);
1702 if (PEAR::IsError($aPointPolygon))
1704 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1707 if ($aPointPolygon['place_id'])
1709 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1710 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1711 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1712 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1714 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1716 $aResult['lat'] = $aPointPolygon['centrelat'];
1717 $aResult['lon'] = $aPointPolygon['centrelon'];
1720 if ($this->bIncludePolygonAsPoints)
1722 $aPolyPoints[] = geometryText2Points($aPointPolygon['astext'],$fRadius);
1724 // Output data suitable for display (points and a bounding box)
1725 if (isset($aPolyPoints))
1727 $aResult['aPolyPoints'] = array();
1728 foreach($aPolyPoints as $aPoint)
1730 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1735 if (abs($aPointPolygon['minlat'] - $aPointPolygon['maxlat']) < 0.0000001)
1737 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1738 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1740 if (abs($aPointPolygon['minlon'] - $aPointPolygon['maxlon']) < 0.0000001)
1742 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1743 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1745 $aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
1749 if ($aResult['extra_place'] == 'city')
1751 $aResult['class'] = 'place';
1752 $aResult['type'] = 'city';
1753 $aResult['rank_search'] = 16;
1756 if (!isset($aResult['aBoundingBox']))
1758 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1759 $fStepSize = (2*pi())/$iSteps;
1760 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1761 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1762 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1763 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1765 // Output data suitable for display (points and a bounding box)
1766 if ($this->bIncludePolygonAsPoints)
1768 $aPolyPoints = array();
1769 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1771 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1773 $aResult['aPolyPoints'] = array();
1774 foreach($aPolyPoints as $aPoint)
1776 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1779 $aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
1782 // Is there an icon set for this type of result?
1783 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1784 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1786 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1789 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1790 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1792 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1794 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1795 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1797 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1799 /* Implement this function later. if tag '&addressdetails=1' is set in query
1800 if ($this->bIncludeAddressDetails)
1802 * getAddressDetails() is defined in lib.php and uses the SQL function get_addressdata in functions.sql
1803 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1804 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1806 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1810 if ($this->bIncludeExtraTags)
1812 if ($aResult['extra'])
1814 $aResult['sExtraTags'] = json_decode($aResult['extra']);
1818 $aResult['sExtraTags'] = (object) array();
1822 if ($this->bIncludeNameDetails)
1824 if ($aResult['names'])
1826 $aResult['sNameDetails'] = json_decode($aResult['names']);
1830 $aResult['sNameDetails'] = (object) array();
1834 // Adjust importance for the number of exact string matches in the result
1835 $aResult['importance'] = max(0.001,$aResult['importance']);
1837 $sAddress = $aResult['langaddress'];
1838 foreach($aRecheckWords as $i => $sWord)
1840 if (stripos($sAddress, $sWord)!==false)
1843 if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1847 $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
1849 $aResult['name'] = $aResult['langaddress'];
1850 // secondary ordering (for results with same importance (the smaller the better):
1851 // - approximate importance of address parts
1852 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1853 // - number of exact matches from the query
1854 if (isset($this->exactMatchCache[$aResult['place_id']]))
1855 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1856 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1857 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1858 // - importance of the class/type
1859 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1860 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1862 $aResult['foundorder'] += 0.0001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1866 $aResult['foundorder'] += 0.01;
1868 if (CONST_Debug) { var_dump($aResult); }
1869 $aSearchResults[$iResNum] = $aResult;
1871 uasort($aSearchResults, 'byImportance');
1873 $aOSMIDDone = array();
1874 $aClassTypeNameDone = array();
1875 $aToFilter = $aSearchResults;
1876 $aSearchResults = array();
1879 foreach($aToFilter as $iResNum => $aResult)
1881 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1884 $fLat = $aResult['lat'];
1885 $fLon = $aResult['lon'];
1886 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1889 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1890 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1892 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1893 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1894 $aSearchResults[] = $aResult;
1897 // Absolute limit on number of results
1898 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1901 return $aSearchResults;