6 protected $aLangPrefOrder = array();
8 protected $bIncludeAddressDetails = false;
10 protected $bIncludePolygonAsPoints = false;
11 protected $bIncludePolygonAsText = false;
12 protected $bIncludePolygonAsGeoJSON = false;
13 protected $bIncludePolygonAsKML = false;
14 protected $bIncludePolygonAsSVG = false;
16 protected $aExcludePlaceIDs = array();
17 protected $bDeDupe = true;
18 protected $bReverseInPlan = false;
20 protected $iLimit = 20;
21 protected $iFinalLimit = 10;
22 protected $iOffset = 0;
23 protected $bFallback = false;
25 protected $aCountryCodes = false;
26 protected $aNearPoint = false;
28 protected $bBoundedSearch = false;
29 protected $aViewBox = false;
30 protected $aRoutePoints = false;
32 protected $iMaxRank = 20;
33 protected $iMinAddressRank = 0;
34 protected $iMaxAddressRank = 30;
35 protected $aAddressRankList = array();
36 protected $exactMatchCache = array();
38 protected $sAllowedTypesSQLList = false;
40 protected $sQuery = false;
41 protected $aStructuredQuery = false;
43 function Geocode(&$oDB)
48 function setReverseInPlan($bReverse)
50 $this->bReverseInPlan = $bReverse;
53 function setLanguagePreference($aLangPref)
55 $this->aLangPrefOrder = $aLangPref;
58 function setIncludeAddressDetails($bAddressDetails = true)
60 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
63 function getIncludeAddressDetails()
65 return $this->bIncludeAddressDetails;
68 function setIncludePolygonAsPoints($b = true)
70 $this->bIncludePolygonAsPoints = $b;
73 function getIncludePolygonAsPoints()
75 return $this->bIncludePolygonAsPoints;
78 function setIncludePolygonAsText($b = true)
80 $this->bIncludePolygonAsText = $b;
83 function getIncludePolygonAsText()
85 return $this->bIncludePolygonAsText;
88 function setIncludePolygonAsGeoJSON($b = true)
90 $this->bIncludePolygonAsGeoJSON = $b;
93 function setIncludePolygonAsKML($b = true)
95 $this->bIncludePolygonAsKML = $b;
98 function setIncludePolygonAsSVG($b = true)
100 $this->bIncludePolygonAsSVG = $b;
103 function setDeDupe($bDeDupe = true)
105 $this->bDeDupe = (bool)$bDeDupe;
108 function setLimit($iLimit = 10)
110 if ($iLimit > 50) $iLimit = 50;
111 if ($iLimit < 1) $iLimit = 1;
113 $this->iFinalLimit = $iLimit;
114 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
117 function setOffset($iOffset = 0)
119 $this->iOffset = $iOffset;
122 function setFallback($bFallback = true)
124 $this->bFallback = (bool)$bFallback;
127 function setExcludedPlaceIDs($a)
129 // TODO: force to int
130 $this->aExcludePlaceIDs = $a;
133 function getExcludedPlaceIDs()
135 return $this->aExcludePlaceIDs;
138 function setBounded($bBoundedSearch = true)
140 $this->bBoundedSearch = (bool)$bBoundedSearch;
143 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
145 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
148 function getViewBoxString()
150 if (!$this->aViewBox) return null;
151 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
154 function setRoute($aRoutePoints)
156 $this->aRoutePoints = $aRoutePoints;
159 function setFeatureType($sFeatureType)
161 switch($sFeatureType)
164 $this->setRankRange(4, 4);
167 $this->setRankRange(8, 8);
170 $this->setRankRange(14, 16);
173 $this->setRankRange(8, 20);
178 function setRankRange($iMin, $iMax)
180 $this->iMinAddressRank = (int)$iMin;
181 $this->iMaxAddressRank = (int)$iMax;
184 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
186 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
189 function setCountryCodesList($aCountryCodes)
191 $this->aCountryCodes = $aCountryCodes;
194 function setQuery($sQueryString)
196 $this->sQuery = $sQueryString;
197 $this->aStructuredQuery = false;
200 function getQueryString()
202 return $this->sQuery;
206 function loadParamArray($aParams)
208 if (isset($aParams['addressdetails'])) $this->bIncludeAddressDetails = (bool)$aParams['addressdetails'];
209 if (isset($aParams['bounded'])) $this->bBoundedSearch = (bool)$aParams['bounded'];
210 if (isset($aParams['dedupe'])) $this->bDeDupe = (bool)$aParams['dedupe'];
212 if (isset($aParams['limit'])) $this->setLimit((int)$aParams['limit']);
213 if (isset($aParams['offset'])) $this->iOffset = (int)$aParams['offset'];
215 if (isset($aParams['fallback'])) $this->bFallback = (bool)$aParams['fallback'];
217 // List of excluded Place IDs - used for more acurate pageing
218 if (isset($aParams['exclude_place_ids']) && $aParams['exclude_place_ids'])
220 foreach(explode(',',$aParams['exclude_place_ids']) as $iExcludedPlaceID)
222 $iExcludedPlaceID = (int)$iExcludedPlaceID;
223 if ($iExcludedPlaceID) $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
225 $this->aExcludePlaceIDs = $aExcludePlaceIDs;
228 // Only certain ranks of feature
229 if (isset($aParams['featureType'])) $this->setFeatureType($aParams['featureType']);
230 if (isset($aParams['featuretype'])) $this->setFeatureType($aParams['featuretype']);
233 if (isset($aParams['countrycodes']))
235 $aCountryCodes = array();
236 foreach(explode(',',$aParams['countrycodes']) as $sCountryCode)
238 if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
240 $aCountryCodes[] = strtolower($sCountryCode);
243 $this->aCountryCodes = $aCountryCodes;
246 if (isset($aParams['viewboxlbrt']) && $aParams['viewboxlbrt'])
248 $aCoOrdinatesLBRT = explode(',',$aParams['viewboxlbrt']);
249 $this->setViewBox($aCoOrdinatesLBRT[0], $aCoOrdinatesLBRT[1], $aCoOrdinatesLBRT[2], $aCoOrdinatesLBRT[3]);
251 else if (isset($aParams['viewbox']) && $aParams['viewbox'])
253 $aCoOrdinatesLTRB = explode(',',$aParams['viewbox']);
254 $this->setViewBox($aCoOrdinatesLTRB[0], $aCoOrdinatesLTRB[3], $aCoOrdinatesLTRB[2], $aCoOrdinatesLTRB[1]);
257 if (isset($aParams['route']) && $aParams['route'] && isset($aParams['routewidth']) && $aParams['routewidth'])
259 $aPoints = explode(',',$aParams['route']);
260 if (sizeof($aPoints) % 2 != 0)
262 userError("Uneven number of points");
267 foreach($aPoints as $i => $fPoint)
271 $aRoute[] = array((float)$fPoint, $fPrevCoord);
275 $fPrevCoord = (float)$fPoint;
278 $this->aRoutePoints = $aRoute;
282 function setQueryFromParams($aParams)
285 $sQuery = (isset($aParams['q'])?trim($aParams['q']):'');
288 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
289 $this->setReverseInPlan(false);
293 $this->setQuery($sQuery);
297 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
299 $sValue = trim($sValue);
300 if (!$sValue) return false;
301 $this->aStructuredQuery[$sKey] = $sValue;
302 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
304 $this->iMinAddressRank = $iNewMinAddressRank;
305 $this->iMaxAddressRank = $iNewMaxAddressRank;
307 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
311 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
313 $this->sQuery = false;
316 $this->iMinAddressRank = 0;
317 $this->iMaxAddressRank = 30;
318 $this->aAddressRankList = array();
320 $this->aStructuredQuery = array();
321 $this->sAllowedTypesSQLList = '';
323 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
324 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
325 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
326 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
327 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
328 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
329 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
331 if (sizeof($this->aStructuredQuery) > 0)
333 $this->sQuery = join(', ', $this->aStructuredQuery);
334 if ($this->iMaxAddressRank < 30)
336 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
341 function fallbackStructuredQuery()
343 if (!$this->aStructuredQuery) return false;
345 $aParams = $this->aStructuredQuery;
347 if (sizeof($aParams) == 1) return false;
349 $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
351 foreach($aOrderToFallback as $sType)
353 if (isset($aParams[$sType]))
355 unset($aParams[$sType]);
356 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
364 function getDetails($aPlaceIDs)
366 if (sizeof($aPlaceIDs) == 0) return array();
368 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
370 // Get the details for display (is this a redundant extra step?)
371 $sPlaceIDs = join(',',$aPlaceIDs);
373 $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,";
374 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
375 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
376 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
377 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
378 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
379 $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, ";
380 $sSQL .= "(extratags->'place') as extra_place ";
381 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
382 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
383 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
384 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
386 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
387 $sSQL .= "and linked_place_id is null ";
388 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
389 if (!$this->bDeDupe) $sSQL .= ",place_id";
390 $sSQL .= ",langaddress ";
391 $sSQL .= ",placename ";
393 $sSQL .= ",extratags->'place' ";
395 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
398 $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,";
399 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
400 $sSQL .= "null as placename,";
401 $sSQL .= "null as ref,";
402 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
403 $sSQL .= "-0.15 as importance, ";
404 $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, ";
405 $sSQL .= "null as extra_place ";
406 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
407 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
408 $sSQL .= "group by place_id";
409 if (!$this->bDeDupe) $sSQL .= ",place_id";
411 $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,";
412 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
413 $sSQL .= "null as placename,";
414 $sSQL .= "null as ref,";
415 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
416 $sSQL .= "-0.10 as importance, ";
417 $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, ";
418 $sSQL .= "null as extra_place ";
419 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
420 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
421 $sSQL .= "group by place_id";
422 if (!$this->bDeDupe) $sSQL .= ",place_id";
423 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
426 $sSQL .= "order by importance desc";
427 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
428 $aSearchResults = $this->oDB->getAll($sSQL);
430 if (PEAR::IsError($aSearchResults))
432 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
435 return $aSearchResults;
438 /* Perform the actual query lookup.
440 Returns an ordered list of results, each with the following fields:
441 osm_type: type of corresponding OSM object
445 P - postcode (internally computed)
446 osm_id: id of corresponding OSM object
447 class: general object class (corresponds to tag key of primary OSM tag)
448 type: subclass of object (corresponds to tag value of primary OSM tag)
449 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
450 rank_search: rank in search hierarchy
451 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
452 rank_address: rank in address hierarchy (determines orer in address)
453 place_id: internal key (may differ between different instances)
454 country_code: ISO country code
455 langaddress: localized full address
456 placename: localized name of object
457 ref: content of ref tag (if available)
460 importance: importance of place based on Wikipedia link count
461 addressimportance: cumulated importance of address elements
462 extra_place: type of place (for admin boundaries, if there is a place tag)
463 aBoundingBox: bounding Box
464 label: short description of the object class/type (English only)
465 name: full name (currently the same as langaddress)
466 foundorder: secondary ordering for places with same importance
470 if (!$this->sQuery && !$this->aStructuredQuery) return false;
472 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
474 $sCountryCodesSQL = false;
475 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
477 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
480 // Hack to make it handle "new york, ny" (and variants) correctly
481 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
483 // Conflicts between US state abreviations and various words for 'the' in different languages
484 if (isset($this->aLangPrefOrder['name:en']))
486 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
487 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
488 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
492 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
493 $bBoundingBoxSearch = false;
496 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
497 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
498 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
499 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
500 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
501 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
503 $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)";
504 $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
505 $bBoundingBoxSearch = $this->bBoundedSearch;
509 if ($this->aRoutePoints)
511 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
513 foreach($this->aRoutePoints as $aPoint)
515 if (!$bFirst) $sViewboxCentreSQL .= ",";
516 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
519 $sViewboxCentreSQL .= ")'::geometry,4326)";
521 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
522 $sViewboxSmallSQL = $this->oDB->getOne($sSQL);
523 if (PEAR::isError($sViewboxSmallSQL))
525 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
527 $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
529 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
530 $sViewboxLargeSQL = $this->oDB->getOne($sSQL);
531 if (PEAR::isError($sViewboxLargeSQL))
533 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
535 $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
536 $bBoundingBoxSearch = $this->bBoundedSearch;
539 // Do we have anything that looks like a lat/lon pair?
540 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
542 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
543 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
544 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
546 $this->setNearPoint(array($fQueryLat, $fQueryLon));
547 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
550 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
552 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
553 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
554 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
556 $this->setNearPoint(array($fQueryLat, $fQueryLon));
557 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
560 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
562 $fQueryLat = $aData[2];
563 $fQueryLon = $aData[3];
564 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
566 $this->setNearPoint(array($fQueryLat, $fQueryLon));
567 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
571 $aSearchResults = array();
572 if ($sQuery || $this->aStructuredQuery)
574 // Start with a blank search
576 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
577 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
578 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
581 // Do we have a radius search?
582 $sNearPointSQL = false;
583 if ($this->aNearPoint)
585 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
586 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
587 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
588 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
591 // Any 'special' terms in the search?
592 $bSpecialTerms = false;
593 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
594 $aSpecialTerms = array();
595 foreach($aSpecialTermsRaw as $aSpecialTerm)
597 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
598 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
601 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
602 $aSpecialTerms = array();
603 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
605 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
606 unset($aStructuredQuery['amenity']);
608 foreach($aSpecialTermsRaw as $aSpecialTerm)
610 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
611 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
612 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
613 $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';
614 if (CONST_Debug) var_Dump($sSQL);
615 $aSearchWords = $this->oDB->getAll($sSQL);
616 $aNewSearches = array();
617 foreach($aSearches as $aSearch)
619 foreach($aSearchWords as $aSearchTerm)
621 $aNewSearch = $aSearch;
622 if ($aSearchTerm['country_code'])
624 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
625 $aNewSearches[] = $aNewSearch;
626 $bSpecialTerms = true;
628 if ($aSearchTerm['class'])
630 $aNewSearch['sClass'] = $aSearchTerm['class'];
631 $aNewSearch['sType'] = $aSearchTerm['type'];
632 $aNewSearches[] = $aNewSearch;
633 $bSpecialTerms = true;
637 $aSearches = $aNewSearches;
640 // Split query into phrases
641 // Commas are used to reduce the search space by indicating where phrases split
642 if ($this->aStructuredQuery)
644 $aPhrases = $this->aStructuredQuery;
645 $bStructuredPhrases = true;
649 $aPhrases = explode(',',$sQuery);
650 $bStructuredPhrases = false;
653 // Convert each phrase to standard form
654 // Create a list of standard words
655 // Get all 'sets' of words
656 // Generate a complete list of all
658 foreach($aPhrases as $iPhrase => $sPhrase)
660 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
661 if (PEAR::isError($aPhrase))
663 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
664 if (CONST_Debug) var_dump($aPhrase);
667 if (trim($aPhrase['string']))
669 $aPhrases[$iPhrase] = $aPhrase;
670 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
671 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
672 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
676 unset($aPhrases[$iPhrase]);
680 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
681 $aPhraseTypes = array_keys($aPhrases);
682 $aPhrases = array_values($aPhrases);
684 if (sizeof($aTokens))
686 // Check which tokens we have, get the ID numbers
687 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
688 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
690 if (CONST_Debug) var_Dump($sSQL);
692 $aValidTokens = array();
693 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
694 else $aDatabaseWords = array();
695 if (PEAR::IsError($aDatabaseWords))
697 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
699 $aPossibleMainWordIDs = array();
700 $aWordFrequencyScores = array();
701 foreach($aDatabaseWords as $aToken)
703 // Very special case - require 2 letter country param to match the country code found
704 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
705 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
710 if (isset($aValidTokens[$aToken['word_token']]))
712 $aValidTokens[$aToken['word_token']][] = $aToken;
716 $aValidTokens[$aToken['word_token']] = array($aToken);
718 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
719 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
721 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
723 // Try and calculate GB postcodes we might be missing
724 foreach($aTokens as $sToken)
726 // Source of gb postcodes is now definitive - always use
727 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
729 if (substr($aData[1],-2,1) != ' ')
731 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
732 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
734 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
735 if ($aGBPostcodeLocation)
737 $aValidTokens[$sToken] = $aGBPostcodeLocation;
740 // US ZIP+4 codes - if there is no token,
741 // merge in the 5-digit ZIP code
742 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
744 if (isset($aValidTokens[$aData[1]]))
746 foreach($aValidTokens[$aData[1]] as $aToken)
748 if (!$aToken['class'])
750 if (isset($aValidTokens[$sToken]))
752 $aValidTokens[$sToken][] = $aToken;
756 $aValidTokens[$sToken] = array($aToken);
764 foreach($aTokens as $sToken)
766 // Unknown single word token with a number - assume it is a house number
767 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
769 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
773 // Any words that have failed completely?
776 // Start the search process
777 $aResultPlaceIDs = array();
780 Calculate all searches using aValidTokens i.e.
781 'Wodsworth Road, Sheffield' =>
785 0 1 (wodsworth)(road)
788 Score how good the search is so they can be ordered
790 foreach($aPhrases as $iPhrase => $sPhrase)
792 $aNewPhraseSearches = array();
793 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
794 else $sPhraseType = '';
796 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
798 // Too many permutations - too expensive
799 if ($iWordSet > 120) break;
801 $aWordsetSearches = $aSearches;
803 // Add all words from this wordset
804 foreach($aWordset as $iToken => $sToken)
806 //echo "<br><b>$sToken</b>";
807 $aNewWordsetSearches = array();
809 foreach($aWordsetSearches as $aCurrentSearch)
812 //var_dump($aCurrentSearch);
815 // If the token is valid
816 if (isset($aValidTokens[' '.$sToken]))
818 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
820 $aSearch = $aCurrentSearch;
821 $aSearch['iSearchRank']++;
822 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
824 if ($aSearch['sCountryCode'] === false)
826 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
827 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
828 // If reverse order is enabled, it may appear at the beginning as well.
829 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
830 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
832 $aSearch['iSearchRank'] += 5;
834 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
837 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
839 if ($aSearch['fLat'] === '')
841 $aSearch['fLat'] = $aSearchTerm['lat'];
842 $aSearch['fLon'] = $aSearchTerm['lon'];
843 $aSearch['fRadius'] = $aSearchTerm['radius'];
844 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
847 elseif ($sPhraseType == 'postalcode')
849 // 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
850 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
852 // If we already have a name try putting the postcode first
853 if (sizeof($aSearch['aName']))
855 $aNewSearch = $aSearch;
856 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
857 $aNewSearch['aName'] = array();
858 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
859 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
862 if (sizeof($aSearch['aName']))
864 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
866 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
870 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
871 $aSearch['iSearchRank'] += 1000; // skip;
876 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
877 //$aSearch['iNamePhrase'] = $iPhrase;
879 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
883 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
885 if ($aSearch['sHouseNumber'] === '')
887 $aSearch['sHouseNumber'] = $sToken;
888 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
890 // Fall back to not searching for this item (better than nothing)
891 $aSearch = $aCurrentSearch;
892 $aSearch['iSearchRank'] += 1;
893 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
897 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
899 if ($aSearch['sClass'] === '')
901 $aSearch['sOperator'] = $aSearchTerm['operator'];
902 $aSearch['sClass'] = $aSearchTerm['class'];
903 $aSearch['sType'] = $aSearchTerm['type'];
904 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
905 else $aSearch['sOperator'] = 'near'; // near = in for the moment
907 // Do we have a shortcut id?
908 if ($aSearch['sOperator'] == 'name')
910 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
911 if ($iAmenityID = $this->oDB->getOne($sSQL))
913 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
914 $aSearch['aName'][$iAmenityID] = $iAmenityID;
915 $aSearch['sClass'] = '';
916 $aSearch['sType'] = '';
919 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
922 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
924 if (sizeof($aSearch['aName']))
926 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
928 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
932 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
933 $aSearch['iSearchRank'] += 1000; // skip;
938 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
939 //$aSearch['iNamePhrase'] = $iPhrase;
941 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
945 if (isset($aValidTokens[$sToken]))
947 // Allow searching for a word - but at extra cost
948 foreach($aValidTokens[$sToken] as $aSearchTerm)
950 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
952 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
954 $aSearch = $aCurrentSearch;
955 $aSearch['iSearchRank'] += 1;
956 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
958 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
959 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
961 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
963 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
965 if (empty($aSearchTermToken['country_code'])
966 && empty($aSearchTermToken['lat'])
967 && empty($aSearchTermToken['class']))
969 $aSearch = $aCurrentSearch;
970 $aSearch['iSearchRank'] += 1;
971 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
972 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
978 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
979 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
983 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
985 $aSearch = $aCurrentSearch;
986 $aSearch['iSearchRank'] += 2;
987 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
988 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
989 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
991 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
992 $aSearch['iNamePhrase'] = $iPhrase;
993 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
1000 // Allow skipping a word - but at EXTREAM cost
1001 //$aSearch = $aCurrentSearch;
1002 //$aSearch['iSearchRank']+=100;
1003 //$aNewWordsetSearches[] = $aSearch;
1007 usort($aNewWordsetSearches, 'bySearchRank');
1008 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
1010 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
1012 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
1013 usort($aNewPhraseSearches, 'bySearchRank');
1015 $aSearchHash = array();
1016 foreach($aNewPhraseSearches as $iSearch => $aSearch)
1018 $sHash = serialize($aSearch);
1019 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
1020 else $aSearchHash[$sHash] = 1;
1023 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
1026 // Re-group the searches by their score, junk anything over 20 as just not worth trying
1027 $aGroupedSearches = array();
1028 foreach($aNewPhraseSearches as $aSearch)
1030 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1032 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1033 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1036 ksort($aGroupedSearches);
1039 $aSearches = array();
1040 foreach($aGroupedSearches as $iScore => $aNewSearches)
1042 $iSearchCount += sizeof($aNewSearches);
1043 $aSearches = array_merge($aSearches, $aNewSearches);
1044 if ($iSearchCount > 50) break;
1047 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1054 // Re-group the searches by their score, junk anything over 20 as just not worth trying
1055 $aGroupedSearches = array();
1056 foreach($aSearches as $aSearch)
1058 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1060 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1061 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1064 ksort($aGroupedSearches);
1067 if (CONST_Debug) var_Dump($aGroupedSearches);
1069 if ($this->bReverseInPlan)
1071 $aCopyGroupedSearches = $aGroupedSearches;
1072 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1074 foreach($aSearches as $iSearch => $aSearch)
1076 if (sizeof($aSearch['aAddress']))
1078 $iReverseItem = array_pop($aSearch['aAddress']);
1079 if (isset($aPossibleMainWordIDs[$iReverseItem]))
1081 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
1082 $aSearch['aName'] = array($iReverseItem);
1083 $aGroupedSearches[$iGroup][] = $aSearch;
1085 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
1086 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
1092 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
1094 $aCopyGroupedSearches = $aGroupedSearches;
1095 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1097 foreach($aSearches as $iSearch => $aSearch)
1099 $aReductionsList = array($aSearch['aAddress']);
1100 $iSearchRank = $aSearch['iSearchRank'];
1101 while(sizeof($aReductionsList) > 0)
1104 if ($iSearchRank > iMaxRank) break 3;
1105 $aNewReductionsList = array();
1106 foreach($aReductionsList as $aReductionsWordList)
1108 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1110 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1111 $aReverseSearch = $aSearch;
1112 $aSearch['aAddress'] = $aReductionsWordListResult;
1113 $aSearch['iSearchRank'] = $iSearchRank;
1114 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1115 if (sizeof($aReductionsWordListResult) > 0)
1117 $aNewReductionsList[] = $aReductionsWordListResult;
1121 $aReductionsList = $aNewReductionsList;
1125 ksort($aGroupedSearches);
1128 // Filter out duplicate searches
1129 $aSearchHash = array();
1130 foreach($aGroupedSearches as $iGroup => $aSearches)
1132 foreach($aSearches as $iSearch => $aSearch)
1134 $sHash = serialize($aSearch);
1135 if (isset($aSearchHash[$sHash]))
1137 unset($aGroupedSearches[$iGroup][$iSearch]);
1138 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1142 $aSearchHash[$sHash] = 1;
1147 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1151 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1154 foreach($aSearches as $aSearch)
1158 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1159 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1161 // No location term?
1162 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1164 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1166 // Just looking for a country by code - look it up
1167 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1169 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1170 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1171 $sSQL .= " order by st_area(geometry) desc limit 1";
1172 if (CONST_Debug) var_dump($sSQL);
1173 $aPlaceIDs = $this->oDB->getCol($sSQL);
1178 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1179 if (!$aSearch['sClass']) continue;
1180 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1181 if ($this->oDB->getOne($sSQL))
1183 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1184 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1185 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
1186 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1187 if (sizeof($this->aExcludePlaceIDs))
1189 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1191 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1192 $sSQL .= " limit $this->iLimit";
1193 if (CONST_Debug) var_dump($sSQL);
1194 $aPlaceIDs = $this->oDB->getCol($sSQL);
1196 // If excluded place IDs are given, it is fair to assume that
1197 // there have been results in the small box, so no further
1198 // expansion in that case.
1199 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1201 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1202 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1203 $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
1204 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1205 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1206 $sSQL .= " limit $this->iLimit";
1207 if (CONST_Debug) var_dump($sSQL);
1208 $aPlaceIDs = $this->oDB->getCol($sSQL);
1213 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1214 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
1215 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1216 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1217 $sSQL .= " limit $this->iLimit";
1218 if (CONST_Debug) var_dump($sSQL);
1219 $aPlaceIDs = $this->oDB->getCol($sSQL);
1225 $aPlaceIDs = array();
1227 // First we need a position, either aName or fLat or both
1231 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1232 // they might be right - but they are just too darned expensive to run
1233 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1234 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1235 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1237 // For infrequent name terms disable index usage for address
1238 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1239 sizeof($aSearch['aName']) == 1 &&
1240 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1242 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1246 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1247 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1250 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1251 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1252 if ($aSearch['fLon'] && $aSearch['fLat'])
1254 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1255 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1257 if (sizeof($this->aExcludePlaceIDs))
1259 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1261 if ($sCountryCodesSQL)
1263 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1266 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
1267 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1269 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1270 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1271 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1272 $aOrder[] = "$sImportanceSQL DESC";
1273 if (sizeof($aSearch['aFullNameAddress']))
1275 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1276 $aOrder[] = 'exactmatch DESC';
1278 $sExactMatchSQL = '0::int as exactmatch';
1281 if (sizeof($aTerms))
1283 $sSQL = "select place_id, ";
1284 $sSQL .= $sExactMatchSQL;
1285 $sSQL .= " from search_name";
1286 $sSQL .= " where ".join(' and ',$aTerms);
1287 $sSQL .= " order by ".join(', ',$aOrder);
1288 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1289 $sSQL .= " limit 50";
1290 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1291 $sSQL .= " limit 1";
1293 $sSQL .= " limit ".$this->iLimit;
1295 if (CONST_Debug) { var_dump($sSQL); }
1296 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1297 if (PEAR::IsError($aViewBoxPlaceIDs))
1299 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1301 //var_dump($aViewBoxPlaceIDs);
1302 // Did we have an viewbox matches?
1303 $aPlaceIDs = array();
1304 $bViewBoxMatch = false;
1305 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1307 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1308 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1309 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1310 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1311 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1312 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1315 //var_Dump($aPlaceIDs);
1318 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1320 $aRoadPlaceIDs = $aPlaceIDs;
1321 $sPlaceIDs = join(',',$aPlaceIDs);
1323 // Now they are indexed look for a house attached to a street we found
1324 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1325 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1326 if (sizeof($this->aExcludePlaceIDs))
1328 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1330 $sSQL .= " limit $this->iLimit";
1331 if (CONST_Debug) var_dump($sSQL);
1332 $aPlaceIDs = $this->oDB->getCol($sSQL);
1334 // If not try the aux fallback table
1335 if (!sizeof($aPlaceIDs))
1337 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1338 if (sizeof($this->aExcludePlaceIDs))
1340 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1342 //$sSQL .= " limit $this->iLimit";
1343 if (CONST_Debug) var_dump($sSQL);
1344 $aPlaceIDs = $this->oDB->getCol($sSQL);
1347 if (!sizeof($aPlaceIDs))
1349 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1350 if (sizeof($this->aExcludePlaceIDs))
1352 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1354 //$sSQL .= " limit $this->iLimit";
1355 if (CONST_Debug) var_dump($sSQL);
1356 $aPlaceIDs = $this->oDB->getCol($sSQL);
1359 // Fallback to the road
1360 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1362 $aPlaceIDs = $aRoadPlaceIDs;
1367 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1369 $sPlaceIDs = join(',',$aPlaceIDs);
1370 $aClassPlaceIDs = array();
1372 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1374 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1375 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1376 $sSQL .= " and linked_place_id is null";
1377 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1378 $sSQL .= " order by rank_search asc limit $this->iLimit";
1379 if (CONST_Debug) var_dump($sSQL);
1380 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1383 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1385 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1386 $bCacheTable = $this->oDB->getOne($sSQL);
1388 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1390 if (CONST_Debug) var_dump($sSQL);
1391 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1393 // For state / country level searches the normal radius search doesn't work very well
1394 $sPlaceGeom = false;
1395 if ($this->iMaxRank < 9 && $bCacheTable)
1397 // Try and get a polygon to search in instead
1398 $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";
1399 if (CONST_Debug) var_dump($sSQL);
1400 $sPlaceGeom = $this->oDB->getOne($sSQL);
1409 $this->iMaxRank += 5;
1410 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1411 if (CONST_Debug) var_dump($sSQL);
1412 $aPlaceIDs = $this->oDB->getCol($sSQL);
1413 $sPlaceIDs = join(',',$aPlaceIDs);
1416 if ($sPlaceIDs || $sPlaceGeom)
1422 // More efficient - can make the range bigger
1426 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1427 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1428 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1430 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1431 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1434 $sSQL .= ",placex as f where ";
1435 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1440 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1442 if (sizeof($this->aExcludePlaceIDs))
1444 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1446 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1447 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1448 if ($iOffset) $sSQL .= " offset $iOffset";
1449 $sSQL .= " limit $this->iLimit";
1450 if (CONST_Debug) var_dump($sSQL);
1451 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1455 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1458 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1459 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1461 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1462 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1463 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1464 if (sizeof($this->aExcludePlaceIDs))
1466 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1468 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1469 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1470 if ($iOffset) $sSQL .= " offset $iOffset";
1471 $sSQL .= " limit $this->iLimit";
1472 if (CONST_Debug) var_dump($sSQL);
1473 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1478 $aPlaceIDs = $aClassPlaceIDs;
1484 if (PEAR::IsError($aPlaceIDs))
1486 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1489 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1491 foreach($aPlaceIDs as $iPlaceID)
1493 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1495 if ($iQueryLoop > 20) break;
1498 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1500 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1501 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1502 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1503 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1504 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1505 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1506 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1507 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1509 if (CONST_Debug) var_dump($sSQL);
1510 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1514 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1515 if ($iGroupLoop > 4) break;
1516 if ($iQueryLoop > 30) break;
1519 // Did we find anything?
1520 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1522 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1528 // Just interpret as a reverse geocode
1529 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1531 $aSearchResults = $this->getDetails(array($iPlaceID));
1533 $aSearchResults = array();
1537 if (!sizeof($aSearchResults))
1539 if ($this->bFallback)
1541 if ($this->fallbackStructuredQuery())
1543 return $this->lookup();
1550 $aClassType = getClassTypesWithImportance();
1551 $aRecheckWords = preg_split('/\b/u',$sQuery);
1552 foreach($aRecheckWords as $i => $sWord)
1554 if (!$sWord) unset($aRecheckWords[$i]);
1557 foreach($aSearchResults as $iResNum => $aResult)
1559 if (CONST_Search_AreaPolygons)
1561 // Get the bounding box and outline polygon
1562 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1563 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1564 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1565 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1566 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1567 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1568 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1569 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1570 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1571 $aPointPolygon = $this->oDB->getRow($sSQL);
1572 if (PEAR::IsError($aPointPolygon))
1574 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1577 if ($aPointPolygon['place_id'])
1579 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1580 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1581 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1582 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1584 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1586 $aResult['lat'] = $aPointPolygon['centrelat'];
1587 $aResult['lon'] = $aPointPolygon['centrelon'];
1590 if ($this->bIncludePolygonAsPoints)
1592 // Translate geometary string to point array
1593 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1595 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1597 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1599 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1601 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1604 $iSteps = ($fRadius * 40000)^2;
1605 $fStepSize = (2*pi())/$iSteps;
1606 $aPolyPoints = array();
1607 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1609 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1611 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1612 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1613 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1614 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1618 // Output data suitable for display (points and a bounding box)
1619 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1621 $aResult['aPolyPoints'] = array();
1622 foreach($aPolyPoints as $aPoint)
1624 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1627 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1631 if ($aResult['extra_place'] == 'city')
1633 $aResult['class'] = 'place';
1634 $aResult['type'] = 'city';
1635 $aResult['rank_search'] = 16;
1638 if (!isset($aResult['aBoundingBox']))
1641 $fDiameter = 0.0001;
1643 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1644 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1646 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1648 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1649 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1651 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1653 $fRadius = $fDiameter / 2;
1655 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1656 $fStepSize = (2*pi())/$iSteps;
1657 $aPolyPoints = array();
1658 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1660 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1662 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1663 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1664 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1665 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1667 // Output data suitable for display (points and a bounding box)
1668 if ($this->bIncludePolygonAsPoints)
1670 $aResult['aPolyPoints'] = array();
1671 foreach($aPolyPoints as $aPoint)
1673 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1676 $aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
1679 // Is there an icon set for this type of result?
1680 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1681 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1683 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1686 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1687 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1689 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1691 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1692 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1694 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1697 if ($this->bIncludeAddressDetails)
1699 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1700 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1702 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1706 // Adjust importance for the number of exact string matches in the result
1707 $aResult['importance'] = max(0.001,$aResult['importance']);
1709 $sAddress = $aResult['langaddress'];
1710 foreach($aRecheckWords as $i => $sWord)
1712 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1715 $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
1717 $aResult['name'] = $aResult['langaddress'];
1718 // secondary ordering (for results with same importance (the smaller the better):
1719 // - approximate importance of address parts
1720 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1721 // - number of exact matches from the query
1722 if (isset($this->exactMatchCache[$aResult['place_id']]))
1723 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1724 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1725 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1726 // - importance of the class/type
1727 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1728 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1730 $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1734 $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1736 $aSearchResults[$iResNum] = $aResult;
1738 uasort($aSearchResults, 'byImportance');
1740 $aOSMIDDone = array();
1741 $aClassTypeNameDone = array();
1742 $aToFilter = $aSearchResults;
1743 $aSearchResults = array();
1746 foreach($aToFilter as $iResNum => $aResult)
1748 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1749 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1752 $fLat = $aResult['lat'];
1753 $fLon = $aResult['lon'];
1754 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1757 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1758 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1760 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1761 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1762 $aSearchResults[] = $aResult;
1765 // Absolute limit on number of results
1766 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1769 return $aSearchResults;
1778 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1780 $aPoints = explode(',',$_GET['route']);
1781 if (sizeof($aPoints) % 2 != 0)
1783 userError("Uneven number of points");
1786 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1787 $fPrevCoord = false;