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 = true;
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;
207 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
209 $sValue = trim($sValue);
210 if (!$sValue) return false;
211 $this->aStructuredQuery[$sKey] = $sValue;
212 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
214 $this->iMinAddressRank = $iNewMinAddressRank;
215 $this->iMaxAddressRank = $iNewMaxAddressRank;
217 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
221 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
223 $this->sQuery = false;
226 $this->iMinAddressRank = 0;
227 $this->iMaxAddressRank = 30;
228 $this->aAddressRankList = array();
230 $this->aStructuredQuery = array();
231 $this->sAllowedTypesSQLList = '';
233 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
234 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
235 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
236 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
237 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
238 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
239 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
241 if (sizeof($this->aStructuredQuery) > 0)
243 $this->sQuery = join(', ', $this->aStructuredQuery);
244 if ($this->iMaxAddressRank < 30)
246 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
251 function fallbackStructuredQuery()
253 if (!$this->aStructuredQuery) return false;
255 $aParams = $this->aStructuredQuery;
257 if (sizeof($aParams) == 1) return false;
259 $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
261 foreach($aOrderToFallback as $sType)
263 if (isset($aParams[$sType]))
265 unset($aParams[$sType]);
266 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
274 function getDetails($aPlaceIDs)
276 if (sizeof($aPlaceIDs) == 0) return array();
278 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
280 // Get the details for display (is this a redundant extra step?)
281 $sPlaceIDs = join(',',$aPlaceIDs);
283 $sImportanceSQL = '';
284 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
285 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
287 $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,";
288 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
289 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
290 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
291 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
292 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
293 $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, ";
294 $sSQL .= "(extratags->'place') as extra_place ";
295 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
296 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
297 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
298 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
300 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
301 $sSQL .= "and linked_place_id is null ";
302 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
303 if (!$this->bDeDupe) $sSQL .= ",place_id";
304 $sSQL .= ",langaddress ";
305 $sSQL .= ",placename ";
307 $sSQL .= ",extratags->'place' ";
309 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
312 $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,";
313 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
314 $sSQL .= "null as placename,";
315 $sSQL .= "null as ref,";
316 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
317 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
318 $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, ";
319 $sSQL .= "null as extra_place ";
320 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
321 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
322 $sSQL .= "group by place_id";
323 if (!$this->bDeDupe) $sSQL .= ",place_id ";
326 $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,";
327 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
328 $sSQL .= "null as placename,";
329 $sSQL .= "null as ref,";
330 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
331 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
332 $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, ";
333 $sSQL .= "null as extra_place ";
334 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
335 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
336 $sSQL .= "group by place_id";
337 if (!$this->bDeDupe) $sSQL .= ",place_id";
338 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
342 $sSQL .= " order by importance desc";
343 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
344 $aSearchResults = $this->oDB->getAll($sSQL);
346 if (PEAR::IsError($aSearchResults))
348 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
351 return $aSearchResults;
354 /* Perform the actual query lookup.
356 Returns an ordered list of results, each with the following fields:
357 osm_type: type of corresponding OSM object
361 P - postcode (internally computed)
362 osm_id: id of corresponding OSM object
363 class: general object class (corresponds to tag key of primary OSM tag)
364 type: subclass of object (corresponds to tag value of primary OSM tag)
365 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
366 rank_search: rank in search hierarchy
367 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
368 rank_address: rank in address hierarchy (determines orer in address)
369 place_id: internal key (may differ between different instances)
370 country_code: ISO country code
371 langaddress: localized full address
372 placename: localized name of object
373 ref: content of ref tag (if available)
376 importance: importance of place based on Wikipedia link count
377 addressimportance: cumulated importance of address elements
378 extra_place: type of place (for admin boundaries, if there is a place tag)
379 aBoundingBox: bounding Box
380 label: short description of the object class/type (English only)
381 name: full name (currently the same as langaddress)
382 foundorder: secondary ordering for places with same importance
386 if (!$this->sQuery && !$this->aStructuredQuery) return false;
388 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
390 $sCountryCodesSQL = false;
391 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
393 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
396 // Hack to make it handle "new york, ny" (and variants) correctly
397 //$sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
398 $sQuery = $this->sQuery;
400 // Conflicts between US state abreviations and various words for 'the' in different languages
401 if (isset($this->aLangPrefOrder['name:en']))
403 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
404 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
405 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
410 $bBoundingBoxSearch = false;
413 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
414 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
415 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
416 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
417 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
418 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
420 $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)";
421 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
422 $bBoundingBoxSearch = $this->bBoundedSearch;
426 if ($this->aRoutePoints)
428 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
430 foreach($this->aRouteaPoints as $aPoint)
432 if (!$bFirst) $sViewboxCentreSQL .= ",";
433 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
435 $sViewboxCentreSQL .= ")'::geometry,4326)";
437 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
438 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
439 if (PEAR::isError($this->sViewboxSmallSQL))
441 failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
443 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
445 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
446 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
447 if (PEAR::isError($this->sViewboxLargeSQL))
449 failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
451 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
452 $bBoundingBoxSearch = $this->bBoundedSearch;
455 // Do we have anything that looks like a lat/lon pair?
456 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
458 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
459 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
460 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
462 $this->setNearPoint(array($fQueryLat, $fQueryLon));
463 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
466 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
468 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
469 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
470 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
472 $this->setNearPoint(array($fQueryLat, $fQueryLon));
473 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
476 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
478 $fQueryLat = $aData[2];
479 $fQueryLon = $aData[3];
480 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
482 $this->setNearPoint(array($fQueryLat, $fQueryLon));
483 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
487 $aSearchResults = array();
488 if ($sQuery || $this->aStructuredQuery)
490 // Start with a blank search
492 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
493 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
494 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
497 // Do we have a radius search?
498 $sNearPointSQL = false;
499 if ($this->aNearPoint)
501 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
502 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
503 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
504 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
507 // Any 'special' terms in the search?
508 $bSpecialTerms = false;
509 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
510 $aSpecialTerms = array();
511 foreach($aSpecialTermsRaw as $aSpecialTerm)
513 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
514 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
517 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
518 $aSpecialTerms = array();
519 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
521 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
522 unset($aStructuredQuery['amenity']);
524 foreach($aSpecialTermsRaw as $aSpecialTerm)
526 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
527 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
528 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
529 $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';
530 if (CONST_Debug) var_Dump($sSQL);
531 $aSearchWords = $this->oDB->getAll($sSQL);
532 $aNewSearches = array();
533 foreach($aSearches as $aSearch)
535 foreach($aSearchWords as $aSearchTerm)
537 $aNewSearch = $aSearch;
538 if ($aSearchTerm['country_code'])
540 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
541 $aNewSearches[] = $aNewSearch;
542 $bSpecialTerms = true;
544 if ($aSearchTerm['class'])
546 $aNewSearch['sClass'] = $aSearchTerm['class'];
547 $aNewSearch['sType'] = $aSearchTerm['type'];
548 $aNewSearches[] = $aNewSearch;
549 $bSpecialTerms = true;
553 $aSearches = $aNewSearches;
556 // Split query into phrases
557 // Commas are used to reduce the search space by indicating where phrases split
558 if ($this->aStructuredQuery)
560 $aPhrases = $this->aStructuredQuery;
561 $bStructuredPhrases = true;
565 $aPhrases = explode(',',$sQuery);
566 $bStructuredPhrases = false;
569 // Convert each phrase to standard form
570 // Create a list of standard words
571 // Get all 'sets' of words
572 // Generate a complete list of all
574 foreach($aPhrases as $iPhrase => $sPhrase)
576 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
577 if (PEAR::isError($aPhrase))
579 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
580 if (CONST_Debug) var_dump($aPhrase);
583 if (trim($aPhrase['string']))
585 $aPhrases[$iPhrase] = $aPhrase;
586 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
587 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
588 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
592 unset($aPhrases[$iPhrase]);
596 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
597 $aPhraseTypes = array_keys($aPhrases);
598 $aPhrases = array_values($aPhrases);
600 if (sizeof($aTokens))
602 // Check which tokens we have, get the ID numbers
603 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
604 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
606 if (CONST_Debug) var_Dump($sSQL);
608 $aValidTokens = array();
609 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
610 else $aDatabaseWords = array();
611 if (PEAR::IsError($aDatabaseWords))
613 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
615 $aPossibleMainWordIDs = array();
616 $aWordFrequencyScores = array();
617 foreach($aDatabaseWords as $aToken)
619 // Very special case - require 2 letter country param to match the country code found
620 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
621 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
626 if (isset($aValidTokens[$aToken['word_token']]))
628 $aValidTokens[$aToken['word_token']][] = $aToken;
632 $aValidTokens[$aToken['word_token']] = array($aToken);
634 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
635 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
637 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
639 // Try and calculate GB postcodes we might be missing
640 foreach($aTokens as $sToken)
642 // Source of gb postcodes is now definitive - always use
643 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
645 if (substr($aData[1],-2,1) != ' ')
647 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
648 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
650 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
651 if ($aGBPostcodeLocation)
653 $aValidTokens[$sToken] = $aGBPostcodeLocation;
656 // US ZIP+4 codes - if there is no token,
657 // merge in the 5-digit ZIP code
658 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
660 if (isset($aValidTokens[$aData[1]]))
662 foreach($aValidTokens[$aData[1]] as $aToken)
664 if (!$aToken['class'])
666 if (isset($aValidTokens[$sToken]))
668 $aValidTokens[$sToken][] = $aToken;
672 $aValidTokens[$sToken] = array($aToken);
680 foreach($aTokens as $sToken)
682 // Unknown single word token with a number - assume it is a house number
683 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
685 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
689 // Any words that have failed completely?
692 // Start the search process
693 $aResultPlaceIDs = array();
696 Calculate all searches using aValidTokens i.e.
697 'Wodsworth Road, Sheffield' =>
701 0 1 (wodsworth)(road)
704 Score how good the search is so they can be ordered
706 foreach($aPhrases as $iPhrase => $sPhrase)
708 $aNewPhraseSearches = array();
709 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
710 else $sPhraseType = '';
712 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
714 // Too many permutations - too expensive
715 if ($iWordSet > 120) break;
717 $aWordsetSearches = $aSearches;
719 // Add all words from this wordset
720 foreach($aWordset as $iToken => $sToken)
722 //echo "<br><b>$sToken</b>";
723 $aNewWordsetSearches = array();
725 foreach($aWordsetSearches as $aCurrentSearch)
728 //var_dump($aCurrentSearch);
731 // If the token is valid
732 if (isset($aValidTokens[' '.$sToken]))
734 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
736 $aSearch = $aCurrentSearch;
737 $aSearch['iSearchRank']++;
738 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
740 if ($aSearch['sCountryCode'] === false)
742 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
743 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
744 // If reverse order is enabled, it may appear at the beginning as well.
745 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
746 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
748 $aSearch['iSearchRank'] += 5;
750 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
753 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
755 if ($aSearch['fLat'] === '')
757 $aSearch['fLat'] = $aSearchTerm['lat'];
758 $aSearch['fLon'] = $aSearchTerm['lon'];
759 $aSearch['fRadius'] = $aSearchTerm['radius'];
760 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
763 elseif ($sPhraseType == 'postalcode')
765 // 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
766 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
768 // If we already have a name try putting the postcode first
769 if (sizeof($aSearch['aName']))
771 $aNewSearch = $aSearch;
772 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
773 $aNewSearch['aName'] = array();
774 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
775 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
778 if (sizeof($aSearch['aName']))
780 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
782 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
786 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
787 $aSearch['iSearchRank'] += 1000; // skip;
792 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
793 //$aSearch['iNamePhrase'] = $iPhrase;
795 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
799 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
801 if ($aSearch['sHouseNumber'] === '')
803 $aSearch['sHouseNumber'] = $sToken;
804 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
806 // Fall back to not searching for this item (better than nothing)
807 $aSearch = $aCurrentSearch;
808 $aSearch['iSearchRank'] += 1;
809 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
813 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
815 if ($aSearch['sClass'] === '')
817 $aSearch['sOperator'] = $aSearchTerm['operator'];
818 $aSearch['sClass'] = $aSearchTerm['class'];
819 $aSearch['sType'] = $aSearchTerm['type'];
820 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
821 else $aSearch['sOperator'] = 'near'; // near = in for the moment
822 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
824 // Do we have a shortcut id?
825 if ($aSearch['sOperator'] == 'name')
827 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
828 if ($iAmenityID = $this->oDB->getOne($sSQL))
830 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
831 $aSearch['aName'][$iAmenityID] = $iAmenityID;
832 $aSearch['sClass'] = '';
833 $aSearch['sType'] = '';
836 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
839 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
841 if (sizeof($aSearch['aName']))
843 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
845 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
849 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
850 $aSearch['iSearchRank'] += 1000; // skip;
855 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
856 //$aSearch['iNamePhrase'] = $iPhrase;
858 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
862 if (isset($aValidTokens[$sToken]))
864 // Allow searching for a word - but at extra cost
865 foreach($aValidTokens[$sToken] as $aSearchTerm)
867 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
869 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
871 $aSearch = $aCurrentSearch;
872 $aSearch['iSearchRank'] += 1;
873 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
875 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
876 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
878 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
880 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
882 if (empty($aSearchTermToken['country_code'])
883 && empty($aSearchTermToken['lat'])
884 && empty($aSearchTermToken['class']))
886 $aSearch = $aCurrentSearch;
887 $aSearch['iSearchRank'] += 1;
888 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
889 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
895 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
896 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
900 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
902 $aSearch = $aCurrentSearch;
903 $aSearch['iSearchRank'] += 2;
904 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
905 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
906 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
908 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
909 $aSearch['iNamePhrase'] = $iPhrase;
910 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
917 // Allow skipping a word - but at EXTREAM cost
918 //$aSearch = $aCurrentSearch;
919 //$aSearch['iSearchRank']+=100;
920 //$aNewWordsetSearches[] = $aSearch;
924 usort($aNewWordsetSearches, 'bySearchRank');
925 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
927 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
929 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
930 usort($aNewPhraseSearches, 'bySearchRank');
932 $aSearchHash = array();
933 foreach($aNewPhraseSearches as $iSearch => $aSearch)
935 $sHash = serialize($aSearch);
936 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
937 else $aSearchHash[$sHash] = 1;
940 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
943 // Re-group the searches by their score, junk anything over 20 as just not worth trying
944 $aGroupedSearches = array();
945 foreach($aNewPhraseSearches as $aSearch)
947 if ($aSearch['iSearchRank'] < $this->iMaxRank)
949 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
950 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
953 ksort($aGroupedSearches);
956 $aSearches = array();
957 foreach($aGroupedSearches as $iScore => $aNewSearches)
959 $iSearchCount += sizeof($aNewSearches);
960 $aSearches = array_merge($aSearches, $aNewSearches);
961 if ($iSearchCount > 50) break;
964 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
971 // Re-group the searches by their score, junk anything over 20 as just not worth trying
972 $aGroupedSearches = array();
973 foreach($aSearches as $aSearch)
975 if ($aSearch['iSearchRank'] < $this->iMaxRank)
977 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
978 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
981 ksort($aGroupedSearches);
984 if (CONST_Debug) var_Dump($aGroupedSearches);
986 if ($this->bReverseInPlan)
988 $aCopyGroupedSearches = $aGroupedSearches;
989 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
991 foreach($aSearches as $iSearch => $aSearch)
993 if (sizeof($aSearch['aAddress']))
995 $iReverseItem = array_pop($aSearch['aAddress']);
996 if (isset($aPossibleMainWordIDs[$iReverseItem]))
998 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
999 $aSearch['aName'] = array($iReverseItem);
1000 $aGroupedSearches[$iGroup][] = $aSearch;
1002 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
1003 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
1009 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
1011 $aCopyGroupedSearches = $aGroupedSearches;
1012 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1014 foreach($aSearches as $iSearch => $aSearch)
1016 $aReductionsList = array($aSearch['aAddress']);
1017 $iSearchRank = $aSearch['iSearchRank'];
1018 while(sizeof($aReductionsList) > 0)
1021 if ($iSearchRank > iMaxRank) break 3;
1022 $aNewReductionsList = array();
1023 foreach($aReductionsList as $aReductionsWordList)
1025 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1027 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1028 $aReverseSearch = $aSearch;
1029 $aSearch['aAddress'] = $aReductionsWordListResult;
1030 $aSearch['iSearchRank'] = $iSearchRank;
1031 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1032 if (sizeof($aReductionsWordListResult) > 0)
1034 $aNewReductionsList[] = $aReductionsWordListResult;
1038 $aReductionsList = $aNewReductionsList;
1042 ksort($aGroupedSearches);
1045 // Filter out duplicate searches
1046 $aSearchHash = array();
1047 foreach($aGroupedSearches as $iGroup => $aSearches)
1049 foreach($aSearches as $iSearch => $aSearch)
1051 $sHash = serialize($aSearch);
1052 if (isset($aSearchHash[$sHash]))
1054 unset($aGroupedSearches[$iGroup][$iSearch]);
1055 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1059 $aSearchHash[$sHash] = 1;
1064 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1068 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1071 foreach($aSearches as $aSearch)
1075 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1076 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1078 // No location term?
1079 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1081 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1083 // Just looking for a country by code - look it up
1084 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1086 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1087 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1088 $sSQL .= " order by st_area(geometry) desc limit 1";
1089 if (CONST_Debug) var_dump($sSQL);
1090 $aPlaceIDs = $this->oDB->getCol($sSQL);
1095 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1096 if (!$aSearch['sClass']) continue;
1097 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1098 if ($this->oDB->getOne($sSQL))
1100 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1101 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1102 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1103 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1104 if (sizeof($this->aExcludePlaceIDs))
1106 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1108 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1109 $sSQL .= " limit $this->iLimit";
1110 if (CONST_Debug) var_dump($sSQL);
1111 $aPlaceIDs = $this->oDB->getCol($sSQL);
1113 // If excluded place IDs are given, it is fair to assume that
1114 // there have been results in the small box, so no further
1115 // expansion in that case.
1116 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1118 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1119 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1120 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1121 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1122 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1123 $sSQL .= " limit $this->iLimit";
1124 if (CONST_Debug) var_dump($sSQL);
1125 $aPlaceIDs = $this->oDB->getCol($sSQL);
1130 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1131 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1132 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1133 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1134 $sSQL .= " limit $this->iLimit";
1135 if (CONST_Debug) var_dump($sSQL);
1136 $aPlaceIDs = $this->oDB->getCol($sSQL);
1142 $aPlaceIDs = array();
1144 // First we need a position, either aName or fLat or both
1148 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1149 // they might be right - but they are just too darned expensive to run
1150 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1151 //if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1152 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1154 // For infrequent name terms disable index usage for address
1155 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1156 sizeof($aSearch['aName']) == 1 &&
1157 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1159 //$aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1160 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddress'],",")."]";
1164 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1165 //if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1168 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1169 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1170 if ($aSearch['fLon'] && $aSearch['fLat'])
1172 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1173 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1175 if (sizeof($this->aExcludePlaceIDs))
1177 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1179 if ($sCountryCodesSQL)
1181 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1184 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1185 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1187 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1188 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1189 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1190 $aOrder[] = "$sImportanceSQL DESC";
1191 if (sizeof($aSearch['aFullNameAddress']))
1193 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1194 $aOrder[] = 'exactmatch DESC';
1196 $sExactMatchSQL = '0::int as exactmatch';
1199 if (sizeof($aTerms))
1201 $sSQL = "select place_id, ";
1202 $sSQL .= $sExactMatchSQL;
1203 $sSQL .= " from search_name";
1204 $sSQL .= " where ".join(' and ',$aTerms);
1205 $sSQL .= " order by ".join(', ',$aOrder);
1206 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1207 $sSQL .= " limit 50";
1208 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1209 $sSQL .= " limit 1";
1211 $sSQL .= " limit ".$this->iLimit;
1213 if (CONST_Debug) { var_dump($sSQL); }
1214 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1215 if (PEAR::IsError($aViewBoxPlaceIDs))
1217 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1219 //var_dump($aViewBoxPlaceIDs);
1220 // Did we have an viewbox matches?
1221 $aPlaceIDs = array();
1222 $bViewBoxMatch = false;
1223 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1225 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1226 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1227 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1228 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1229 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1230 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1233 //var_Dump($aPlaceIDs);
1236 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1238 $aRoadPlaceIDs = $aPlaceIDs;
1239 $sPlaceIDs = join(',',$aPlaceIDs);
1241 // Now they are indexed look for a house attached to a street we found
1242 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1243 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1244 if (sizeof($this->aExcludePlaceIDs))
1246 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1248 $sSQL .= " limit $this->iLimit";
1249 if (CONST_Debug) var_dump($sSQL);
1250 $aPlaceIDs = $this->oDB->getCol($sSQL);
1252 // If not try the aux fallback table
1254 if (!sizeof($aPlaceIDs))
1256 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1257 if (sizeof($this->aExcludePlaceIDs))
1259 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1261 //$sSQL .= " limit $this->iLimit";
1262 if (CONST_Debug) var_dump($sSQL);
1263 $aPlaceIDs = $this->oDB->getCol($sSQL);
1267 if (!sizeof($aPlaceIDs))
1269 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1270 if (sizeof($this->aExcludePlaceIDs))
1272 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1274 //$sSQL .= " limit $this->iLimit";
1275 if (CONST_Debug) var_dump($sSQL);
1276 $aPlaceIDs = $this->oDB->getCol($sSQL);
1279 // Fallback to the road
1280 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1282 $aPlaceIDs = $aRoadPlaceIDs;
1287 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1289 $sPlaceIDs = join(',',$aPlaceIDs);
1290 $aClassPlaceIDs = array();
1292 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1294 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1295 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1296 $sSQL .= " and linked_place_id is null";
1297 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1298 $sSQL .= " order by rank_search asc limit $this->iLimit";
1299 if (CONST_Debug) var_dump($sSQL);
1300 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1303 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1305 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1306 $bCacheTable = $this->oDB->getOne($sSQL);
1308 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1310 if (CONST_Debug) var_dump($sSQL);
1311 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1313 // For state / country level searches the normal radius search doesn't work very well
1314 $sPlaceGeom = false;
1315 if ($this->iMaxRank < 9 && $bCacheTable)
1317 // Try and get a polygon to search in instead
1318 $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";
1319 if (CONST_Debug) var_dump($sSQL);
1320 $sPlaceGeom = $this->oDB->getOne($sSQL);
1329 $this->iMaxRank += 5;
1330 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1331 if (CONST_Debug) var_dump($sSQL);
1332 $aPlaceIDs = $this->oDB->getCol($sSQL);
1333 $sPlaceIDs = join(',',$aPlaceIDs);
1336 if ($sPlaceIDs || $sPlaceGeom)
1342 // More efficient - can make the range bigger
1346 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1347 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1348 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1350 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1351 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1354 $sSQL .= ",placex as f where ";
1355 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1360 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1362 if (sizeof($this->aExcludePlaceIDs))
1364 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1366 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1367 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1368 if ($iOffset) $sSQL .= " offset $iOffset";
1369 $sSQL .= " limit $this->iLimit";
1370 if (CONST_Debug) var_dump($sSQL);
1371 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1375 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1378 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1379 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1381 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1382 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1383 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1384 if (sizeof($this->aExcludePlaceIDs))
1386 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1388 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1389 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1390 if ($iOffset) $sSQL .= " offset $iOffset";
1391 $sSQL .= " limit $this->iLimit";
1392 if (CONST_Debug) var_dump($sSQL);
1393 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1398 $aPlaceIDs = $aClassPlaceIDs;
1404 if (PEAR::IsError($aPlaceIDs))
1406 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1409 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1411 foreach($aPlaceIDs as $iPlaceID)
1413 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1415 if ($iQueryLoop > 20) break;
1418 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1420 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1421 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1422 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1423 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1424 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1425 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1426 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1427 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1429 if (CONST_Debug) var_dump($sSQL);
1430 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1434 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1435 if ($iGroupLoop > 4) break;
1436 if ($iQueryLoop > 30) break;
1439 // Did we find anything?
1440 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1442 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1448 // Just interpret as a reverse geocode
1449 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1451 $aSearchResults = $this->getDetails(array($iPlaceID));
1453 $aSearchResults = array();
1457 if (!sizeof($aSearchResults))
1459 if ($this->bFallback)
1461 if ($this->fallbackStructuredQuery())
1463 return $this->lookup();
1470 $aClassType = getClassTypesWithImportance();
1471 $aRecheckWords = preg_split('/\b/u',$sQuery);
1472 foreach($aRecheckWords as $i => $sWord)
1474 if (!$sWord) unset($aRecheckWords[$i]);
1477 foreach($aSearchResults as $iResNum => $aResult)
1479 if (CONST_Search_AreaPolygons)
1481 // Get the bounding box and outline polygon
1482 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1483 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1484 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1485 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1486 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1487 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1488 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1489 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1490 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1491 $aPointPolygon = $this->oDB->getRow($sSQL);
1492 if (PEAR::IsError($aPointPolygon))
1494 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1497 if ($aPointPolygon['place_id'])
1499 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1500 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1501 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1502 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1504 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1506 $aResult['lat'] = $aPointPolygon['centrelat'];
1507 $aResult['lon'] = $aPointPolygon['centrelon'];
1510 if ($this->bIncludePolygonAsPoints)
1512 // Translate geometary string to point array
1513 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1515 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1518 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1520 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1523 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1526 $iSteps = ($fRadius * 40000)^2;
1527 $fStepSize = (2*pi())/$iSteps;
1528 $aPolyPoints = array();
1529 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1531 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1533 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1534 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1535 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1536 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1540 // Output data suitable for display (points and a bounding box)
1541 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1543 $aResult['aPolyPoints'] = array();
1544 foreach($aPolyPoints as $aPoint)
1546 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1549 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1553 if ($aResult['extra_place'] == 'city')
1555 $aResult['class'] = 'place';
1556 $aResult['type'] = 'city';
1557 $aResult['rank_search'] = 16;
1560 if (!isset($aResult['aBoundingBox']))
1563 $fDiameter = 0.0001;
1565 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1566 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1568 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1570 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1571 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1573 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1575 $fRadius = $fDiameter / 2;
1577 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1578 $fStepSize = (2*pi())/$iSteps;
1579 $aPolyPoints = array();
1580 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1582 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1584 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1585 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1586 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1587 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1589 // Output data suitable for display (points and a bounding box)
1590 if ($this->bIncludePolygonAsPoints)
1592 $aResult['aPolyPoints'] = array();
1593 foreach($aPolyPoints as $aPoint)
1595 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1598 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1601 // Is there an icon set for this type of result?
1602 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1603 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1605 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1608 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1609 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1611 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1613 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1614 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1616 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1619 if ($this->bIncludeAddressDetails)
1621 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1622 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1624 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1628 // Adjust importance for the number of exact string matches in the result
1629 $aResult['importance'] = max(0.001,$aResult['importance']);
1631 $sAddress = $aResult['langaddress'];
1632 foreach($aRecheckWords as $i => $sWord)
1634 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1637 $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
1639 $aResult['name'] = $aResult['langaddress'];
1640 // secondary ordering (for results with same importance (the smaller the better):
1641 // - approximate importance of address parts
1642 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1643 // - number of exact matches from the query
1644 if (isset($this->exactMatchCache[$aResult['place_id']]))
1645 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1646 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1647 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1648 // - importance of the class/type
1649 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1650 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1652 $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1656 $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1658 $aSearchResults[$iResNum] = $aResult;
1660 uasort($aSearchResults, 'byImportance');
1662 $aOSMIDDone = array();
1663 $aClassTypeNameDone = array();
1664 $aToFilter = $aSearchResults;
1665 $aSearchResults = array();
1668 foreach($aToFilter as $iResNum => $aResult)
1670 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1671 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1674 $fLat = $aResult['lat'];
1675 $fLon = $aResult['lon'];
1676 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1679 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1680 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1682 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1683 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1684 $aSearchResults[] = $aResult;
1687 // Absolute limit on number of results
1688 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1691 return $aSearchResults;
1700 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1702 $aPoints = explode(',',$_GET['route']);
1703 if (sizeof($aPoints) % 2 != 0)
1705 userError("Uneven number of points");
1708 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1709 $fPrevCoord = false;