6 protected $aLangPrefOrder = array();
8 protected $bIncludeAddressDetails = false;
10 protected $bIncludePolygonAsPoints = false;
11 protected $bIncludePolygonAsText = false;
12 protected $bIncludePolygonAsGeoJSON = false;
13 protected $bIncludePolygonAsKML = false;
14 protected $bIncludePolygonAsSVG = false;
16 protected $aExcludePlaceIDs = array();
17 protected $bDeDupe = true;
18 protected $bReverseInPlan = true;
20 protected $iLimit = 20;
21 protected $iFinalLimit = 10;
22 protected $iOffset = 0;
24 protected $aCountryCodes = false;
25 protected $aNearPoint = false;
27 protected $bBoundedSearch = false;
28 protected $aViewBox = false;
29 protected $sViewboxSmallSQL = false;
30 protected $sViewboxLargeSQL = false;
31 protected $aRoutePoints = false;
33 protected $iMaxRank = 20;
34 protected $iMinAddressRank = 0;
35 protected $iMaxAddressRank = 30;
36 protected $aAddressRankList = array();
37 protected $exactMatchCache = array();
39 protected $sAllowedTypesSQLList = false;
41 protected $sQuery = false;
42 protected $aStructuredQuery = false;
44 function Geocode(&$oDB)
49 function setReverseInPlan($bReverse)
51 $this->bReverseInPlan = $bReverse;
54 function setLanguagePreference($aLangPref)
56 $this->aLangPrefOrder = $aLangPref;
59 function setIncludeAddressDetails($bAddressDetails = true)
61 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
64 function getIncludeAddressDetails()
66 return $this->bIncludeAddressDetails;
69 function setIncludePolygonAsPoints($b = true)
71 $this->bIncludePolygonAsPoints = $b;
74 function getIncludePolygonAsPoints()
76 return $this->bIncludePolygonAsPoints;
79 function setIncludePolygonAsText($b = true)
81 $this->bIncludePolygonAsText = $b;
84 function getIncludePolygonAsText()
86 return $this->bIncludePolygonAsText;
89 function setIncludePolygonAsGeoJSON($b = true)
91 $this->bIncludePolygonAsGeoJSON = $b;
94 function setIncludePolygonAsKML($b = true)
96 $this->bIncludePolygonAsKML = $b;
99 function setIncludePolygonAsSVG($b = true)
101 $this->bIncludePolygonAsSVG = $b;
104 function setDeDupe($bDeDupe = true)
106 $this->bDeDupe = (bool)$bDeDupe;
109 function setLimit($iLimit = 10)
111 if ($iLimit > 50) $iLimit = 50;
112 if ($iLimit < 1) $iLimit = 1;
114 $this->iFinalLimit = $iLimit;
115 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
118 function setOffset($iOffset = 0)
120 $this->iOffset = $iOffset;
123 function setExcludedPlaceIDs($a)
125 // TODO: force to int
126 $this->aExcludePlaceIDs = $a;
129 function getExcludedPlaceIDs()
131 return $this->aExcludePlaceIDs;
134 function setBounded($bBoundedSearch = true)
136 $this->bBoundedSearch = (bool)$bBoundedSearch;
139 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
141 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
144 function getViewBoxString()
146 if (!$this->aViewBox) return null;
147 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
150 function setRoute($aRoutePoints)
152 $this->aRoutePoints = $aRoutePoints;
155 function setFeatureType($sFeatureType)
157 switch($sFeatureType)
160 $this->setRankRange(4, 4);
163 $this->setRankRange(8, 8);
166 $this->setRankRange(14, 16);
169 $this->setRankRange(8, 20);
174 function setRankRange($iMin, $iMax)
176 $this->iMinAddressRank = (int)$iMin;
177 $this->iMaxAddressRank = (int)$iMax;
180 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
182 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
185 function setCountryCodesList($aCountryCodes)
187 $this->aCountryCodes = $aCountryCodes;
190 function setQuery($sQueryString)
192 $this->sQuery = $sQueryString;
193 $this->aStructuredQuery = false;
196 function getQueryString()
198 return $this->sQuery;
201 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
203 $sValue = trim($sValue);
204 if (!$sValue) return false;
205 $this->aStructuredQuery[$sKey] = $sValue;
206 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
208 $this->iMinAddressRank = $iNewMinAddressRank;
209 $this->iMaxAddressRank = $iNewMaxAddressRank;
211 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
215 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
217 $this->sQuery = false;
219 $this->aStructuredQuery = array();
220 $this->sAllowedTypesSQLList = '';
222 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
223 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
224 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
225 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
226 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
227 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
228 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
230 if (sizeof($this->aStructuredQuery) > 0)
232 $this->sQuery = join(', ', $this->aStructuredQuery);
233 if ($this->iMaxAddressRank < 30)
235 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
241 function getDetails($aPlaceIDs)
243 if (sizeof($aPlaceIDs) == 0) return array();
245 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
247 // Get the details for display (is this a redundant extra step?)
248 $sPlaceIDs = join(',',$aPlaceIDs);
250 $sImportanceSQL = '';
251 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
252 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
254 $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,";
255 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
256 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
257 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
258 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
259 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
260 $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, ";
261 $sSQL .= "(extratags->'place') as extra_place ";
262 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
263 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
264 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
265 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
267 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
268 $sSQL .= "and linked_place_id is null ";
269 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
270 if (!$this->bDeDupe) $sSQL .= ",place_id";
271 $sSQL .= ",langaddress ";
272 $sSQL .= ",placename ";
274 $sSQL .= ",extratags->'place' ";
276 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
279 $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,";
280 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
281 $sSQL .= "null as placename,";
282 $sSQL .= "null as ref,";
283 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
284 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
285 $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, ";
286 $sSQL .= "null as extra_place ";
287 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
288 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
289 $sSQL .= "group by place_id";
290 if (!$this->bDeDupe) $sSQL .= ",place_id ";
293 $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,";
294 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
295 $sSQL .= "null as placename,";
296 $sSQL .= "null as ref,";
297 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
298 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
299 $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, ";
300 $sSQL .= "null as extra_place ";
301 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
302 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
303 $sSQL .= "group by place_id";
304 if (!$this->bDeDupe) $sSQL .= ",place_id";
305 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
309 $sSQL .= " order by importance desc";
310 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
311 $aSearchResults = $this->oDB->getAll($sSQL);
313 if (PEAR::IsError($aSearchResults))
315 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
318 return $aSearchResults;
321 /* Perform the actual query lookup.
323 Returns an ordered list of results, each with the following fields:
324 osm_type: type of corresponding OSM object
328 P - postcode (internally computed)
329 osm_id: id of corresponding OSM object
330 class: general object class (corresponds to tag key of primary OSM tag)
331 type: subclass of object (corresponds to tag value of primary OSM tag)
332 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
333 rank_search: rank in search hierarchy
334 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
335 rank_address: rank in address hierarchy (determines orer in address)
336 place_id: internal key (may differ between different instances)
337 country_code: ISO country code
338 langaddress: localized full address
339 placename: localized name of object
340 ref: content of ref tag (if available)
343 importance: importance of place based on Wikipedia link count
344 addressimportance: cumulated importance of address elements
345 extra_place: type of place (for admin boundaries, if there is a place tag)
346 aBoundingBox: bounding Box
347 label: short description of the object class/type (English only)
348 name: full name (currently the same as langaddress)
349 foundorder: secondary ordering for places with same importance
353 if (!$this->sQuery && !$this->aStructuredQuery) return false;
355 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
357 $sCountryCodesSQL = false;
358 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
360 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
363 // Hack to make it handle "new york, ny" (and variants) correctly
364 //$sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
365 $sQuery = $this->sQuery;
367 // Conflicts between US state abreviations and various words for 'the' in different languages
368 if (isset($this->aLangPrefOrder['name:en']))
370 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
371 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
372 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
377 $bBoundingBoxSearch = false;
380 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
381 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
382 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
383 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
384 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
385 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
387 $this->sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$this->aViewBox[0].",".(float)$this->aViewBox[1]."),ST_Point(".(float)$this->aViewBox[2].",".(float)$this->aViewBox[3].")),4326)";
388 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
389 $bBoundingBoxSearch = $this->bBoundedSearch;
393 if ($this->aRoutePoints)
395 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
397 foreach($this->aRouteaPoints as $aPoint)
399 if (!$bFirst) $sViewboxCentreSQL .= ",";
400 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
402 $sViewboxCentreSQL .= ")'::geometry,4326)";
404 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
405 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
406 if (PEAR::isError($this->sViewboxSmallSQL))
408 failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
410 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
412 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
413 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
414 if (PEAR::isError($this->sViewboxLargeSQL))
416 failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
418 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
419 $bBoundingBoxSearch = $this->bBoundedSearch;
422 // Do we have anything that looks like a lat/lon pair?
423 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
425 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
426 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
427 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
429 $this->setNearPoint(array($fQueryLat, $fQueryLon));
430 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
433 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
435 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
436 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
437 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
439 $this->setNearPoint(array($fQueryLat, $fQueryLon));
440 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
443 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
445 $fQueryLat = $aData[2];
446 $fQueryLon = $aData[3];
447 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
449 $this->setNearPoint(array($fQueryLat, $fQueryLon));
450 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
454 $aSearchResults = array();
455 if ($sQuery || $this->aStructuredQuery)
457 // Start with a blank search
459 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
460 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
461 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
464 // Do we have a radius search?
465 $sNearPointSQL = false;
466 if ($this->aNearPoint)
468 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
469 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
470 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
471 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
474 // Any 'special' terms in the search?
475 $bSpecialTerms = false;
476 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
477 $aSpecialTerms = array();
478 foreach($aSpecialTermsRaw as $aSpecialTerm)
480 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
481 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
484 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
485 $aSpecialTerms = array();
486 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
488 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
489 unset($aStructuredQuery['amenity']);
491 foreach($aSpecialTermsRaw as $aSpecialTerm)
493 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
494 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
495 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
496 $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';
497 if (CONST_Debug) var_Dump($sSQL);
498 $aSearchWords = $this->oDB->getAll($sSQL);
499 $aNewSearches = array();
500 foreach($aSearches as $aSearch)
502 foreach($aSearchWords as $aSearchTerm)
504 $aNewSearch = $aSearch;
505 if ($aSearchTerm['country_code'])
507 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
508 $aNewSearches[] = $aNewSearch;
509 $bSpecialTerms = true;
511 if ($aSearchTerm['class'])
513 $aNewSearch['sClass'] = $aSearchTerm['class'];
514 $aNewSearch['sType'] = $aSearchTerm['type'];
515 $aNewSearches[] = $aNewSearch;
516 $bSpecialTerms = true;
520 $aSearches = $aNewSearches;
523 // Split query into phrases
524 // Commas are used to reduce the search space by indicating where phrases split
525 if ($this->aStructuredQuery)
527 $aPhrases = $this->aStructuredQuery;
528 $bStructuredPhrases = true;
532 $aPhrases = explode(',',$sQuery);
533 $bStructuredPhrases = false;
536 // Convert each phrase to standard form
537 // Create a list of standard words
538 // Get all 'sets' of words
539 // Generate a complete list of all
541 foreach($aPhrases as $iPhrase => $sPhrase)
543 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
544 if (PEAR::isError($aPhrase))
546 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
547 if (CONST_Debug) var_dump($aPhrase);
550 if (trim($aPhrase['string']))
552 $aPhrases[$iPhrase] = $aPhrase;
553 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
554 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
555 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
559 unset($aPhrases[$iPhrase]);
563 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
564 $aPhraseTypes = array_keys($aPhrases);
565 $aPhrases = array_values($aPhrases);
567 if (sizeof($aTokens))
569 // Check which tokens we have, get the ID numbers
570 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
571 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
573 if (CONST_Debug) var_Dump($sSQL);
575 $aValidTokens = array();
576 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
577 else $aDatabaseWords = array();
578 if (PEAR::IsError($aDatabaseWords))
580 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
582 $aPossibleMainWordIDs = array();
583 $aWordFrequencyScores = array();
584 foreach($aDatabaseWords as $aToken)
586 // Very special case - require 2 letter country param to match the country code found
587 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
588 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
593 if (isset($aValidTokens[$aToken['word_token']]))
595 $aValidTokens[$aToken['word_token']][] = $aToken;
599 $aValidTokens[$aToken['word_token']] = array($aToken);
601 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
602 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
604 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
606 // Try and calculate GB postcodes we might be missing
607 foreach($aTokens as $sToken)
609 // Source of gb postcodes is now definitive - always use
610 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
612 if (substr($aData[1],-2,1) != ' ')
614 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
615 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
617 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
618 if ($aGBPostcodeLocation)
620 $aValidTokens[$sToken] = $aGBPostcodeLocation;
623 // US ZIP+4 codes - if there is no token,
624 // merge in the 5-digit ZIP code
625 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
627 if (isset($aValidTokens[$aData[1]]))
629 foreach($aValidTokens[$aData[1]] as $aToken)
631 if (!$aToken['class'])
633 if (isset($aValidTokens[$sToken]))
635 $aValidTokens[$sToken][] = $aToken;
639 $aValidTokens[$sToken] = array($aToken);
647 foreach($aTokens as $sToken)
649 // Unknown single word token with a number - assume it is a house number
650 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
652 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
656 // Any words that have failed completely?
659 // Start the search process
660 $aResultPlaceIDs = array();
663 Calculate all searches using aValidTokens i.e.
664 'Wodsworth Road, Sheffield' =>
668 0 1 (wodsworth)(road)
671 Score how good the search is so they can be ordered
673 foreach($aPhrases as $iPhrase => $sPhrase)
675 $aNewPhraseSearches = array();
676 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
677 else $sPhraseType = '';
679 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
681 // Too many permutations - too expensive
682 if ($iWordSet > 120) break;
684 $aWordsetSearches = $aSearches;
686 // Add all words from this wordset
687 foreach($aWordset as $iToken => $sToken)
689 //echo "<br><b>$sToken</b>";
690 $aNewWordsetSearches = array();
692 foreach($aWordsetSearches as $aCurrentSearch)
695 //var_dump($aCurrentSearch);
698 // If the token is valid
699 if (isset($aValidTokens[' '.$sToken]))
701 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
703 $aSearch = $aCurrentSearch;
704 $aSearch['iSearchRank']++;
705 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
707 if ($aSearch['sCountryCode'] === false)
709 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
710 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
711 // If reverse order is enabled, it may appear at the beginning as well.
712 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
713 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
715 $aSearch['iSearchRank'] += 5;
717 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
720 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
722 if ($aSearch['fLat'] === '')
724 $aSearch['fLat'] = $aSearchTerm['lat'];
725 $aSearch['fLon'] = $aSearchTerm['lon'];
726 $aSearch['fRadius'] = $aSearchTerm['radius'];
727 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
730 elseif ($sPhraseType == 'postalcode')
732 // 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
733 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
735 // If we already have a name try putting the postcode first
736 if (sizeof($aSearch['aName']))
738 $aNewSearch = $aSearch;
739 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
740 $aNewSearch['aName'] = array();
741 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
742 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
745 if (sizeof($aSearch['aName']))
747 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
749 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
753 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
754 $aSearch['iSearchRank'] += 1000; // skip;
759 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
760 //$aSearch['iNamePhrase'] = $iPhrase;
762 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
766 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
768 if ($aSearch['sHouseNumber'] === '')
770 $aSearch['sHouseNumber'] = $sToken;
771 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
773 // Fall back to not searching for this item (better than nothing)
774 $aSearch = $aCurrentSearch;
775 $aSearch['iSearchRank'] += 1;
776 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
780 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
782 if ($aSearch['sClass'] === '')
784 $aSearch['sOperator'] = $aSearchTerm['operator'];
785 $aSearch['sClass'] = $aSearchTerm['class'];
786 $aSearch['sType'] = $aSearchTerm['type'];
787 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
788 else $aSearch['sOperator'] = 'near'; // near = in for the moment
789 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
791 // Do we have a shortcut id?
792 if ($aSearch['sOperator'] == 'name')
794 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
795 if ($iAmenityID = $this->oDB->getOne($sSQL))
797 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
798 $aSearch['aName'][$iAmenityID] = $iAmenityID;
799 $aSearch['sClass'] = '';
800 $aSearch['sType'] = '';
803 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
806 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
808 if (sizeof($aSearch['aName']))
810 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
812 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
816 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
817 $aSearch['iSearchRank'] += 1000; // skip;
822 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
823 //$aSearch['iNamePhrase'] = $iPhrase;
825 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
829 if (isset($aValidTokens[$sToken]))
831 // Allow searching for a word - but at extra cost
832 foreach($aValidTokens[$sToken] as $aSearchTerm)
834 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
836 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
838 $aSearch = $aCurrentSearch;
839 $aSearch['iSearchRank'] += 1;
840 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
842 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
843 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
845 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
847 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
849 if (empty($aSearchTermToken['country_code'])
850 && empty($aSearchTermToken['lat'])
851 && empty($aSearchTermToken['class']))
853 $aSearch = $aCurrentSearch;
854 $aSearch['iSearchRank'] += 1;
855 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
856 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
862 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
863 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
867 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
869 $aSearch = $aCurrentSearch;
870 $aSearch['iSearchRank'] += 2;
871 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
872 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
873 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
875 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
876 $aSearch['iNamePhrase'] = $iPhrase;
877 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
884 // Allow skipping a word - but at EXTREAM cost
885 //$aSearch = $aCurrentSearch;
886 //$aSearch['iSearchRank']+=100;
887 //$aNewWordsetSearches[] = $aSearch;
891 usort($aNewWordsetSearches, 'bySearchRank');
892 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
894 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
896 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
897 usort($aNewPhraseSearches, 'bySearchRank');
899 $aSearchHash = array();
900 foreach($aNewPhraseSearches as $iSearch => $aSearch)
902 $sHash = serialize($aSearch);
903 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
904 else $aSearchHash[$sHash] = 1;
907 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
910 // Re-group the searches by their score, junk anything over 20 as just not worth trying
911 $aGroupedSearches = array();
912 foreach($aNewPhraseSearches as $aSearch)
914 if ($aSearch['iSearchRank'] < $this->iMaxRank)
916 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
917 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
920 ksort($aGroupedSearches);
923 $aSearches = array();
924 foreach($aGroupedSearches as $iScore => $aNewSearches)
926 $iSearchCount += sizeof($aNewSearches);
927 $aSearches = array_merge($aSearches, $aNewSearches);
928 if ($iSearchCount > 50) break;
931 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
938 // Re-group the searches by their score, junk anything over 20 as just not worth trying
939 $aGroupedSearches = array();
940 foreach($aSearches as $aSearch)
942 if ($aSearch['iSearchRank'] < $this->iMaxRank)
944 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
945 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
948 ksort($aGroupedSearches);
951 if (CONST_Debug) var_Dump($aGroupedSearches);
953 if ($this->bReverseInPlan)
955 $aCopyGroupedSearches = $aGroupedSearches;
956 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
958 foreach($aSearches as $iSearch => $aSearch)
960 if (sizeof($aSearch['aAddress']))
962 $iReverseItem = array_pop($aSearch['aAddress']);
963 if (isset($aPossibleMainWordIDs[$iReverseItem]))
965 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
966 $aSearch['aName'] = array($iReverseItem);
967 $aGroupedSearches[$iGroup][] = $aSearch;
969 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
970 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
976 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
978 $aCopyGroupedSearches = $aGroupedSearches;
979 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
981 foreach($aSearches as $iSearch => $aSearch)
983 $aReductionsList = array($aSearch['aAddress']);
984 $iSearchRank = $aSearch['iSearchRank'];
985 while(sizeof($aReductionsList) > 0)
988 if ($iSearchRank > iMaxRank) break 3;
989 $aNewReductionsList = array();
990 foreach($aReductionsList as $aReductionsWordList)
992 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
994 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
995 $aReverseSearch = $aSearch;
996 $aSearch['aAddress'] = $aReductionsWordListResult;
997 $aSearch['iSearchRank'] = $iSearchRank;
998 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
999 if (sizeof($aReductionsWordListResult) > 0)
1001 $aNewReductionsList[] = $aReductionsWordListResult;
1005 $aReductionsList = $aNewReductionsList;
1009 ksort($aGroupedSearches);
1012 // Filter out duplicate searches
1013 $aSearchHash = array();
1014 foreach($aGroupedSearches as $iGroup => $aSearches)
1016 foreach($aSearches as $iSearch => $aSearch)
1018 $sHash = serialize($aSearch);
1019 if (isset($aSearchHash[$sHash]))
1021 unset($aGroupedSearches[$iGroup][$iSearch]);
1022 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1026 $aSearchHash[$sHash] = 1;
1031 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1035 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1038 foreach($aSearches as $aSearch)
1042 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1043 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1045 // No location term?
1046 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1048 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1050 // Just looking for a country by code - look it up
1051 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1053 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1054 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1055 $sSQL .= " order by st_area(geometry) desc limit 1";
1056 if (CONST_Debug) var_dump($sSQL);
1057 $aPlaceIDs = $this->oDB->getCol($sSQL);
1062 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1063 if (!$aSearch['sClass']) continue;
1064 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1065 if ($this->oDB->getOne($sSQL))
1067 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1068 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1069 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1070 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1071 if (sizeof($this->aExcludePlaceIDs))
1073 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1075 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1076 $sSQL .= " limit $this->iLimit";
1077 if (CONST_Debug) var_dump($sSQL);
1078 $aPlaceIDs = $this->oDB->getCol($sSQL);
1080 // If excluded place IDs are given, it is fair to assume that
1081 // there have been results in the small box, so no further
1082 // expansion in that case.
1083 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1085 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1086 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1087 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1088 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1089 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1090 $sSQL .= " limit $this->iLimit";
1091 if (CONST_Debug) var_dump($sSQL);
1092 $aPlaceIDs = $this->oDB->getCol($sSQL);
1097 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1098 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1099 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1100 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1101 $sSQL .= " limit $this->iLimit";
1102 if (CONST_Debug) var_dump($sSQL);
1103 $aPlaceIDs = $this->oDB->getCol($sSQL);
1109 $aPlaceIDs = array();
1111 // First we need a position, either aName or fLat or both
1115 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1116 // they might be right - but they are just too darned expensive to run
1117 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1118 //if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1119 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1121 // For infrequent name terms disable index usage for address
1122 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1123 sizeof($aSearch['aName']) == 1 &&
1124 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1126 //$aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1127 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddress'],",")."]";
1131 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1132 //if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1135 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1136 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1137 if ($aSearch['fLon'] && $aSearch['fLat'])
1139 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1140 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1142 if (sizeof($this->aExcludePlaceIDs))
1144 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1146 if ($sCountryCodesSQL)
1148 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1151 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1152 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1154 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1155 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1156 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1157 $aOrder[] = "$sImportanceSQL DESC";
1158 if (sizeof($aSearch['aFullNameAddress']))
1160 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1161 $aOrder[] = 'exactmatch DESC';
1163 $sExactMatchSQL = '0::int as exactmatch';
1166 if (sizeof($aTerms))
1168 $sSQL = "select place_id, ";
1169 $sSQL .= $sExactMatchSQL;
1170 $sSQL .= " from search_name";
1171 $sSQL .= " where ".join(' and ',$aTerms);
1172 $sSQL .= " order by ".join(', ',$aOrder);
1173 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1174 $sSQL .= " limit 50";
1175 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1176 $sSQL .= " limit 1";
1178 $sSQL .= " limit ".$this->iLimit;
1180 if (CONST_Debug) { var_dump($sSQL); }
1181 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1182 if (PEAR::IsError($aViewBoxPlaceIDs))
1184 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1186 //var_dump($aViewBoxPlaceIDs);
1187 // Did we have an viewbox matches?
1188 $aPlaceIDs = array();
1189 $bViewBoxMatch = false;
1190 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1192 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1193 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1194 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1195 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1196 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1197 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1200 //var_Dump($aPlaceIDs);
1203 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1205 $aRoadPlaceIDs = $aPlaceIDs;
1206 $sPlaceIDs = join(',',$aPlaceIDs);
1208 // Now they are indexed look for a house attached to a street we found
1209 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1210 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1211 if (sizeof($this->aExcludePlaceIDs))
1213 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1215 $sSQL .= " limit $this->iLimit";
1216 if (CONST_Debug) var_dump($sSQL);
1217 $aPlaceIDs = $this->oDB->getCol($sSQL);
1219 // If not try the aux fallback table
1221 if (!sizeof($aPlaceIDs))
1223 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1224 if (sizeof($this->aExcludePlaceIDs))
1226 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1228 //$sSQL .= " limit $this->iLimit";
1229 if (CONST_Debug) var_dump($sSQL);
1230 $aPlaceIDs = $this->oDB->getCol($sSQL);
1234 if (!sizeof($aPlaceIDs))
1236 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1237 if (sizeof($this->aExcludePlaceIDs))
1239 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1241 //$sSQL .= " limit $this->iLimit";
1242 if (CONST_Debug) var_dump($sSQL);
1243 $aPlaceIDs = $this->oDB->getCol($sSQL);
1246 // Fallback to the road
1247 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1249 $aPlaceIDs = $aRoadPlaceIDs;
1254 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1256 $sPlaceIDs = join(',',$aPlaceIDs);
1257 $aClassPlaceIDs = array();
1259 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1261 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1262 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1263 $sSQL .= " and linked_place_id is null";
1264 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1265 $sSQL .= " order by rank_search asc limit $this->iLimit";
1266 if (CONST_Debug) var_dump($sSQL);
1267 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1270 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1272 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1273 $bCacheTable = $this->oDB->getOne($sSQL);
1275 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1277 if (CONST_Debug) var_dump($sSQL);
1278 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1280 // For state / country level searches the normal radius search doesn't work very well
1281 $sPlaceGeom = false;
1282 if ($this->iMaxRank < 9 && $bCacheTable)
1284 // Try and get a polygon to search in instead
1285 $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";
1286 if (CONST_Debug) var_dump($sSQL);
1287 $sPlaceGeom = $this->oDB->getOne($sSQL);
1296 $this->iMaxRank += 5;
1297 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1298 if (CONST_Debug) var_dump($sSQL);
1299 $aPlaceIDs = $this->oDB->getCol($sSQL);
1300 $sPlaceIDs = join(',',$aPlaceIDs);
1303 if ($sPlaceIDs || $sPlaceGeom)
1309 // More efficient - can make the range bigger
1313 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1314 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1315 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1317 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1318 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1321 $sSQL .= ",placex as f where ";
1322 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1327 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1329 if (sizeof($this->aExcludePlaceIDs))
1331 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1333 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1334 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1335 if ($iOffset) $sSQL .= " offset $iOffset";
1336 $sSQL .= " limit $this->iLimit";
1337 if (CONST_Debug) var_dump($sSQL);
1338 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1342 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1345 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1346 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1348 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1349 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1350 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1351 if (sizeof($this->aExcludePlaceIDs))
1353 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1355 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1356 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1357 if ($iOffset) $sSQL .= " offset $iOffset";
1358 $sSQL .= " limit $this->iLimit";
1359 if (CONST_Debug) var_dump($sSQL);
1360 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1365 $aPlaceIDs = $aClassPlaceIDs;
1371 if (PEAR::IsError($aPlaceIDs))
1373 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1376 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1378 foreach($aPlaceIDs as $iPlaceID)
1380 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1382 if ($iQueryLoop > 20) break;
1385 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1387 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1388 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1389 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1390 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1391 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1392 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1393 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1394 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1396 if (CONST_Debug) var_dump($sSQL);
1397 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1401 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1402 if ($iGroupLoop > 4) break;
1403 if ($iQueryLoop > 30) break;
1406 // Did we find anything?
1407 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1409 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1415 // Just interpret as a reverse geocode
1416 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1418 $aSearchResults = $this->getDetails(array($iPlaceID));
1420 $aSearchResults = array();
1424 if (!sizeof($aSearchResults))
1429 $aClassType = getClassTypesWithImportance();
1430 $aRecheckWords = preg_split('/\b/u',$sQuery);
1431 foreach($aRecheckWords as $i => $sWord)
1433 if (!$sWord) unset($aRecheckWords[$i]);
1436 foreach($aSearchResults as $iResNum => $aResult)
1438 if (CONST_Search_AreaPolygons)
1440 // Get the bounding box and outline polygon
1441 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1442 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1443 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1444 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1445 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1446 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1447 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1448 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1449 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1450 $aPointPolygon = $this->oDB->getRow($sSQL);
1451 if (PEAR::IsError($aPointPolygon))
1453 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1456 if ($aPointPolygon['place_id'])
1458 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1459 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1460 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1461 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1463 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1465 $aResult['lat'] = $aPointPolygon['centrelat'];
1466 $aResult['lon'] = $aPointPolygon['centrelon'];
1469 if ($this->bIncludePolygonAsPoints)
1471 // Translate geometary string to point array
1472 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1474 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1477 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1479 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1482 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1485 $iSteps = ($fRadius * 40000)^2;
1486 $fStepSize = (2*pi())/$iSteps;
1487 $aPolyPoints = array();
1488 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1490 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1492 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1493 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1494 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1495 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1499 // Output data suitable for display (points and a bounding box)
1500 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1502 $aResult['aPolyPoints'] = array();
1503 foreach($aPolyPoints as $aPoint)
1505 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1508 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1512 if ($aResult['extra_place'] == 'city')
1514 $aResult['class'] = 'place';
1515 $aResult['type'] = 'city';
1516 $aResult['rank_search'] = 16;
1519 if (!isset($aResult['aBoundingBox']))
1522 $fDiameter = 0.0001;
1524 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1525 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1527 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1529 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1530 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1532 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1534 $fRadius = $fDiameter / 2;
1536 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1537 $fStepSize = (2*pi())/$iSteps;
1538 $aPolyPoints = array();
1539 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1541 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1543 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1544 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1545 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1546 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1548 // Output data suitable for display (points and a bounding box)
1549 if ($this->bIncludePolygonAsPoints)
1551 $aResult['aPolyPoints'] = array();
1552 foreach($aPolyPoints as $aPoint)
1554 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1557 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1560 // Is there an icon set for this type of result?
1561 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1562 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1564 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1567 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1568 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1570 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1572 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1573 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1575 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1578 if ($this->bIncludeAddressDetails)
1580 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1581 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1583 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1587 // Adjust importance for the number of exact string matches in the result
1588 $aResult['importance'] = max(0.001,$aResult['importance']);
1590 $sAddress = $aResult['langaddress'];
1591 foreach($aRecheckWords as $i => $sWord)
1593 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1596 $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
1598 $aResult['name'] = $aResult['langaddress'];
1599 // secondary ordering (for results with same importance (the smaller the better):
1600 // - approximate importance of address parts
1601 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1602 // - number of exact matches from the query
1603 if (isset($this->exactMatchCache[$aResult['place_id']]))
1604 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1605 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1606 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1607 // - importance of the class/type
1608 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1609 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1611 $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1615 $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1617 $aSearchResults[$iResNum] = $aResult;
1619 uasort($aSearchResults, 'byImportance');
1621 $aOSMIDDone = array();
1622 $aClassTypeNameDone = array();
1623 $aToFilter = $aSearchResults;
1624 $aSearchResults = array();
1627 foreach($aToFilter as $iResNum => $aResult)
1629 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1630 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1633 $fLat = $aResult['lat'];
1634 $fLon = $aResult['lon'];
1635 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1638 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1639 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1641 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1642 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1643 $aSearchResults[] = $aResult;
1646 // Absolute limit on number of results
1647 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1650 return $aSearchResults;
1659 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1661 $aPoints = explode(',',$_GET['route']);
1662 if (sizeof($aPoints) % 2 != 0)
1664 userError("Uneven number of points");
1667 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1668 $fPrevCoord = false;