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*(,|$)/','\1illinois\2', $sQuery);
487 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/','\1alabama\2', $sQuery);
488 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/','\1louisiana\2', $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 if ($aSearch['sHouseNumber'])
1271 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1275 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1277 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1278 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1279 $aOrder[] = "$sImportanceSQL DESC";
1280 if (sizeof($aSearch['aFullNameAddress']))
1282 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1283 $aOrder[] = 'exactmatch DESC';
1285 $sExactMatchSQL = '0::int as exactmatch';
1288 if (sizeof($aTerms))
1290 $sSQL = "select place_id, ";
1291 $sSQL .= $sExactMatchSQL;
1292 $sSQL .= " from search_name";
1293 $sSQL .= " where ".join(' and ',$aTerms);
1294 $sSQL .= " order by ".join(', ',$aOrder);
1295 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1296 $sSQL .= " limit 50";
1297 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1298 $sSQL .= " limit 1";
1300 $sSQL .= " limit ".$this->iLimit;
1302 if (CONST_Debug) { var_dump($sSQL); }
1303 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1304 if (PEAR::IsError($aViewBoxPlaceIDs))
1306 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1308 //var_dump($aViewBoxPlaceIDs);
1309 // Did we have an viewbox matches?
1310 $aPlaceIDs = array();
1311 $bViewBoxMatch = false;
1312 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1314 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1315 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1316 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1317 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1318 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1319 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1322 //var_Dump($aPlaceIDs);
1325 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1327 $aRoadPlaceIDs = $aPlaceIDs;
1328 $sPlaceIDs = join(',',$aPlaceIDs);
1330 // Now they are indexed look for a house attached to a street we found
1331 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1332 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1333 if (sizeof($this->aExcludePlaceIDs))
1335 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1337 $sSQL .= " limit $this->iLimit";
1338 if (CONST_Debug) var_dump($sSQL);
1339 $aPlaceIDs = $this->oDB->getCol($sSQL);
1341 // If not try the aux fallback table
1342 if (!sizeof($aPlaceIDs))
1344 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1345 if (sizeof($this->aExcludePlaceIDs))
1347 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1349 //$sSQL .= " limit $this->iLimit";
1350 if (CONST_Debug) var_dump($sSQL);
1351 $aPlaceIDs = $this->oDB->getCol($sSQL);
1354 if (!sizeof($aPlaceIDs))
1356 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1357 if (sizeof($this->aExcludePlaceIDs))
1359 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1361 //$sSQL .= " limit $this->iLimit";
1362 if (CONST_Debug) var_dump($sSQL);
1363 $aPlaceIDs = $this->oDB->getCol($sSQL);
1366 // Fallback to the road
1367 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1369 $aPlaceIDs = $aRoadPlaceIDs;
1374 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1376 $sPlaceIDs = join(',',$aPlaceIDs);
1377 $aClassPlaceIDs = array();
1379 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1381 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1382 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1383 $sSQL .= " and linked_place_id is null";
1384 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1385 $sSQL .= " order by rank_search asc limit $this->iLimit";
1386 if (CONST_Debug) var_dump($sSQL);
1387 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1390 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1392 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1393 $bCacheTable = $this->oDB->getOne($sSQL);
1395 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1397 if (CONST_Debug) var_dump($sSQL);
1398 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1400 // For state / country level searches the normal radius search doesn't work very well
1401 $sPlaceGeom = false;
1402 if ($this->iMaxRank < 9 && $bCacheTable)
1404 // Try and get a polygon to search in instead
1405 $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";
1406 if (CONST_Debug) var_dump($sSQL);
1407 $sPlaceGeom = $this->oDB->getOne($sSQL);
1416 $this->iMaxRank += 5;
1417 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1418 if (CONST_Debug) var_dump($sSQL);
1419 $aPlaceIDs = $this->oDB->getCol($sSQL);
1420 $sPlaceIDs = join(',',$aPlaceIDs);
1423 if ($sPlaceIDs || $sPlaceGeom)
1429 // More efficient - can make the range bigger
1433 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1434 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1435 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1437 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1438 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1441 $sSQL .= ",placex as f where ";
1442 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1447 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1449 if (sizeof($this->aExcludePlaceIDs))
1451 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1453 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1454 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1455 if ($iOffset) $sSQL .= " offset $iOffset";
1456 $sSQL .= " limit $this->iLimit";
1457 if (CONST_Debug) var_dump($sSQL);
1458 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1462 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1465 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1466 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1468 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1469 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1470 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1471 if (sizeof($this->aExcludePlaceIDs))
1473 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1475 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1476 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1477 if ($iOffset) $sSQL .= " offset $iOffset";
1478 $sSQL .= " limit $this->iLimit";
1479 if (CONST_Debug) var_dump($sSQL);
1480 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1485 $aPlaceIDs = $aClassPlaceIDs;
1491 if (PEAR::IsError($aPlaceIDs))
1493 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1496 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1498 foreach($aPlaceIDs as $iPlaceID)
1500 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1502 if ($iQueryLoop > 20) break;
1505 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1507 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1508 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1509 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1510 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1511 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1512 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1513 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1514 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1516 if (CONST_Debug) var_dump($sSQL);
1517 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1521 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1522 if ($iGroupLoop > 4) break;
1523 if ($iQueryLoop > 30) break;
1526 // Did we find anything?
1527 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1529 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1535 // Just interpret as a reverse geocode
1536 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1538 $aSearchResults = $this->getDetails(array($iPlaceID));
1540 $aSearchResults = array();
1544 if (!sizeof($aSearchResults))
1546 if ($this->bFallback)
1548 if ($this->fallbackStructuredQuery())
1550 return $this->lookup();
1557 $aClassType = getClassTypesWithImportance();
1558 $aRecheckWords = preg_split('/\b/u',$sQuery);
1559 foreach($aRecheckWords as $i => $sWord)
1561 if (!$sWord) unset($aRecheckWords[$i]);
1564 foreach($aSearchResults as $iResNum => $aResult)
1566 if (CONST_Search_AreaPolygons)
1568 // Get the bounding box and outline polygon
1569 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1570 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1571 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1572 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1573 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1574 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1575 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1576 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1577 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1578 $aPointPolygon = $this->oDB->getRow($sSQL);
1579 if (PEAR::IsError($aPointPolygon))
1581 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1584 if ($aPointPolygon['place_id'])
1586 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1587 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1588 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1589 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1591 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1593 $aResult['lat'] = $aPointPolygon['centrelat'];
1594 $aResult['lon'] = $aPointPolygon['centrelon'];
1597 if ($this->bIncludePolygonAsPoints)
1599 // Translate geometary string to point array
1600 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1602 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1604 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1606 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1608 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1611 $iSteps = ($fRadius * 40000)^2;
1612 $fStepSize = (2*pi())/$iSteps;
1613 $aPolyPoints = array();
1614 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1616 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1618 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1619 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1620 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1621 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1625 // Output data suitable for display (points and a bounding box)
1626 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1628 $aResult['aPolyPoints'] = array();
1629 foreach($aPolyPoints as $aPoint)
1631 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1634 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1638 if ($aResult['extra_place'] == 'city')
1640 $aResult['class'] = 'place';
1641 $aResult['type'] = 'city';
1642 $aResult['rank_search'] = 16;
1645 if (!isset($aResult['aBoundingBox']))
1648 $fDiameter = 0.0001;
1650 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1651 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1653 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1655 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1656 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1658 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1660 $fRadius = $fDiameter / 2;
1662 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1663 $fStepSize = (2*pi())/$iSteps;
1664 $aPolyPoints = array();
1665 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1667 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1669 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1670 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1671 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1672 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1674 // Output data suitable for display (points and a bounding box)
1675 if ($this->bIncludePolygonAsPoints)
1677 $aResult['aPolyPoints'] = array();
1678 foreach($aPolyPoints as $aPoint)
1680 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1683 $aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
1686 // Is there an icon set for this type of result?
1687 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1688 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1690 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1693 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1694 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1696 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1698 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1699 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1701 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1704 if ($this->bIncludeAddressDetails)
1706 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1707 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1709 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1713 // Adjust importance for the number of exact string matches in the result
1714 $aResult['importance'] = max(0.001,$aResult['importance']);
1716 $sAddress = $aResult['langaddress'];
1717 foreach($aRecheckWords as $i => $sWord)
1719 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1722 $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
1724 $aResult['name'] = $aResult['langaddress'];
1725 // secondary ordering (for results with same importance (the smaller the better):
1726 // - approximate importance of address parts
1727 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1728 // - number of exact matches from the query
1729 if (isset($this->exactMatchCache[$aResult['place_id']]))
1730 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1731 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1732 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1733 // - importance of the class/type
1734 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1735 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1737 $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1741 $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1743 $aSearchResults[$iResNum] = $aResult;
1745 uasort($aSearchResults, 'byImportance');
1747 $aOSMIDDone = array();
1748 $aClassTypeNameDone = array();
1749 $aToFilter = $aSearchResults;
1750 $aSearchResults = array();
1753 foreach($aToFilter as $iResNum => $aResult)
1755 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1756 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1759 $fLat = $aResult['lat'];
1760 $fLon = $aResult['lon'];
1761 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1764 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1765 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1767 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1768 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1769 $aSearchResults[] = $aResult;
1772 // Absolute limit on number of results
1773 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1776 return $aSearchResults;
1785 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1787 $aPoints = explode(',',$_GET['route']);
1788 if (sizeof($aPoints) % 2 != 0)
1790 userError("Uneven number of points");
1793 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1794 $fPrevCoord = false;