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();
37 protected $sAllowedTypesSQLList = false;
39 protected $sQuery = false;
40 protected $aStructuredQuery = false;
42 function Geocode(&$oDB)
47 function setReverseInPlan($bReverse)
49 $this->bReverseInPlan = $bReverse;
52 function setLanguagePreference($aLangPref)
54 $this->aLangPrefOrder = $aLangPref;
57 function setIncludeAddressDetails($bAddressDetails = true)
59 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
62 function getIncludeAddressDetails()
64 return $this->bIncludeAddressDetails;
67 function setIncludePolygonAsPoints($b = true)
69 $this->bIncludePolygonAsPoints = $b;
72 function getIncludePolygonAsPoints()
74 return $this->bIncludePolygonAsPoints;
77 function setIncludePolygonAsText($b = true)
79 $this->bIncludePolygonAsText = $b;
82 function getIncludePolygonAsText()
84 return $this->bIncludePolygonAsText;
87 function setIncludePolygonAsGeoJSON($b = true)
89 $this->bIncludePolygonAsGeoJSON = $b;
92 function setIncludePolygonAsKML($b = true)
94 $this->bIncludePolygonAsKML = $b;
97 function setIncludePolygonAsSVG($b = true)
99 $this->bIncludePolygonAsSVG = $b;
102 function setDeDupe($bDeDupe = true)
104 $this->bDeDupe = (bool)$bDeDupe;
107 function setLimit($iLimit = 10)
109 if ($iLimit > 50) $iLimit = 50;
110 if ($iLimit < 1) $iLimit = 1;
112 $this->iFinalLimit = $iLimit;
113 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
116 function setOffset($iOffset = 0)
118 $this->iOffset = $iOffset;
121 function setFallback($bFallback = true)
123 $this->bFallback = (bool)$bFallback;
126 function setExcludedPlaceIDs($a)
128 // TODO: force to int
129 $this->aExcludePlaceIDs = $a;
132 function getExcludedPlaceIDs()
134 return $this->aExcludePlaceIDs;
137 function setBounded($bBoundedSearch = true)
139 $this->bBoundedSearch = (bool)$bBoundedSearch;
142 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
144 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
147 function getViewBoxString()
149 if (!$this->aViewBox) return null;
150 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
153 function setRoute($aRoutePoints)
155 $this->aRoutePoints = $aRoutePoints;
158 function setFeatureType($sFeatureType)
160 switch($sFeatureType)
163 $this->setRankRange(4, 4);
166 $this->setRankRange(8, 8);
169 $this->setRankRange(14, 16);
172 $this->setRankRange(8, 20);
177 function setRankRange($iMin, $iMax)
179 $this->iMinAddressRank = (int)$iMin;
180 $this->iMaxAddressRank = (int)$iMax;
183 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
185 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
188 function setCountryCodesList($aCountryCodes)
190 $this->aCountryCodes = $aCountryCodes;
193 function setQuery($sQueryString)
195 $this->sQuery = $sQueryString;
196 $this->aStructuredQuery = false;
199 function getQueryString()
201 return $this->sQuery;
204 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
206 $sValue = trim($sValue);
207 if (!$sValue) return false;
208 $this->aStructuredQuery[$sKey] = $sValue;
209 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
211 $this->iMinAddressRank = $iNewMinAddressRank;
212 $this->iMaxAddressRank = $iNewMaxAddressRank;
214 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
218 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
220 $this->sQuery = false;
223 $this->iMinAddressRank = 0;
224 $this->iMaxAddressRank = 30;
225 $this->aAddressRankList = array();
227 $this->aStructuredQuery = array();
228 $this->sAllowedTypesSQLList = '';
230 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
231 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
232 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
233 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
234 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
235 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
236 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
238 if (sizeof($this->aStructuredQuery) > 0)
240 $this->sQuery = join(', ', $this->aStructuredQuery);
241 if ($this->iMaxAddressRank < 30)
243 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
248 function fallbackStructuredQuery()
250 if (!$this->aStructuredQuery) return false;
252 $aParams = $this->aStructuredQuery;
254 if (sizeof($aParams) == 1) return false;
256 $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
258 foreach($aOrderToFallback as $sType)
260 if (isset($aParams[$sType]))
262 unset($aParams[$sType]);
263 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
271 function getDetails($aPlaceIDs)
273 if (sizeof($aPlaceIDs) == 0) return array();
275 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
277 // Get the details for display (is this a redundant extra step?)
278 $sPlaceIDs = join(',',$aPlaceIDs);
280 $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id,calculated_country_code as country_code,";
281 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
282 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
283 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
284 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
285 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
286 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(placex.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
287 $sSQL .= "(extratags->'place') as extra_place ";
288 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
289 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
290 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
291 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
293 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
294 $sSQL .= "and linked_place_id is null ";
295 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
296 if (!$this->bDeDupe) $sSQL .= ",place_id";
297 $sSQL .= ",langaddress ";
298 $sSQL .= ",placename ";
300 $sSQL .= ",extratags->'place' ";
302 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
305 $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,'us' as country_code,";
306 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
307 $sSQL .= "null as placename,";
308 $sSQL .= "null as ref,";
309 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
310 $sSQL .= "-0.15 as importance, ";
311 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_tiger.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
312 $sSQL .= "null as extra_place ";
313 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
314 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
315 $sSQL .= "group by place_id";
316 if (!$this->bDeDupe) $sSQL .= ",place_id";
318 $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,'us' as country_code,";
319 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
320 $sSQL .= "null as placename,";
321 $sSQL .= "null as ref,";
322 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
323 $sSQL .= "-0.10 as importance, ";
324 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
325 $sSQL .= "null as extra_place ";
326 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
327 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
328 $sSQL .= "group by place_id";
329 if (!$this->bDeDupe) $sSQL .= ",place_id";
330 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
333 $sSQL .= "order by importance desc";
334 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
335 $aSearchResults = $this->oDB->getAll($sSQL);
337 if (PEAR::IsError($aSearchResults))
339 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
342 return $aSearchResults;
347 if (!$this->sQuery && !$this->aStructuredQuery) return false;
349 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
351 $sCountryCodesSQL = false;
352 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
354 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
357 // Hack to make it handle "new york, ny" (and variants) correctly
358 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
360 // Conflicts between US state abreviations and various words for 'the' in different languages
361 if (isset($this->aLangPrefOrder['name:en']))
363 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
364 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
365 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
369 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
370 $bBoundingBoxSearch = false;
373 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
374 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
375 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
376 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
377 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
378 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
380 $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)";
381 $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
382 $bBoundingBoxSearch = $this->bBoundedSearch;
386 if ($this->aRoutePoints)
388 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
390 foreach($this->aRouteaPoints as $aPoint)
392 if (!$bFirst) $sViewboxCentreSQL .= ",";
393 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
395 $sViewboxCentreSQL .= ")'::geometry,4326)";
397 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
398 $sViewboxSmallSQL = $this->oDB->getOne($sSQL);
399 if (PEAR::isError($sViewboxSmallSQL))
401 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
403 $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
405 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
406 $sViewboxLargeSQL = $this->oDB->getOne($sSQL);
407 if (PEAR::isError($sViewboxLargeSQL))
409 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
411 $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
412 $bBoundingBoxSearch = $this->bBoundedSearch;
415 // Do we have anything that looks like a lat/lon pair?
416 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
418 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
419 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
420 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
422 $this->setNearPoint(array($fQueryLat, $fQueryLon));
423 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
426 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
428 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
429 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
430 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
432 $this->setNearPoint(array($fQueryLat, $fQueryLon));
433 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
436 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
438 $fQueryLat = $aData[2];
439 $fQueryLon = $aData[3];
440 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
442 $this->setNearPoint(array($fQueryLat, $fQueryLon));
443 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
447 $aSearchResults = array();
448 if ($sQuery || $this->aStructuredQuery)
450 // Start with a blank search
452 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
453 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
454 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
457 // Do we have a radius search?
458 $sNearPointSQL = false;
459 if ($this->aNearPoint)
461 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
462 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
463 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
464 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
467 // Any 'special' terms in the search?
468 $bSpecialTerms = false;
469 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
470 $aSpecialTerms = array();
471 foreach($aSpecialTermsRaw as $aSpecialTerm)
473 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
474 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
477 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
478 $aSpecialTerms = array();
479 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
481 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
482 unset($aStructuredQuery['amenity']);
484 foreach($aSpecialTermsRaw as $aSpecialTerm)
486 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
487 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
488 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
489 $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';
490 if (CONST_Debug) var_Dump($sSQL);
491 $aSearchWords = $this->oDB->getAll($sSQL);
492 $aNewSearches = array();
493 foreach($aSearches as $aSearch)
495 foreach($aSearchWords as $aSearchTerm)
497 $aNewSearch = $aSearch;
498 if ($aSearchTerm['country_code'])
500 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
501 $aNewSearches[] = $aNewSearch;
502 $bSpecialTerms = true;
504 if ($aSearchTerm['class'])
506 $aNewSearch['sClass'] = $aSearchTerm['class'];
507 $aNewSearch['sType'] = $aSearchTerm['type'];
508 $aNewSearches[] = $aNewSearch;
509 $bSpecialTerms = true;
513 $aSearches = $aNewSearches;
516 // Split query into phrases
517 // Commas are used to reduce the search space by indicating where phrases split
518 if ($this->aStructuredQuery)
520 $aPhrases = $this->aStructuredQuery;
521 $bStructuredPhrases = true;
525 $aPhrases = explode(',',$sQuery);
526 $bStructuredPhrases = false;
529 // Convert each phrase to standard form
530 // Create a list of standard words
531 // Get all 'sets' of words
532 // Generate a complete list of all
534 foreach($aPhrases as $iPhrase => $sPhrase)
536 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
537 if (PEAR::isError($aPhrase))
539 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
540 if (CONST_Debug) var_dump($aPhrase);
543 if (trim($aPhrase['string']))
545 $aPhrases[$iPhrase] = $aPhrase;
546 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
547 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
548 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
552 unset($aPhrases[$iPhrase]);
556 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
557 $aPhraseTypes = array_keys($aPhrases);
558 $aPhrases = array_values($aPhrases);
560 if (sizeof($aTokens))
562 // Check which tokens we have, get the ID numbers
563 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
564 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
566 if (CONST_Debug) var_Dump($sSQL);
568 $aValidTokens = array();
569 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
570 else $aDatabaseWords = array();
571 if (PEAR::IsError($aDatabaseWords))
573 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
575 $aPossibleMainWordIDs = array();
576 $aWordFrequencyScores = array();
577 foreach($aDatabaseWords as $aToken)
579 // Very special case - require 2 letter country param to match the country code found
580 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
581 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
586 if (isset($aValidTokens[$aToken['word_token']]))
588 $aValidTokens[$aToken['word_token']][] = $aToken;
592 $aValidTokens[$aToken['word_token']] = array($aToken);
594 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
595 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
597 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
599 // Try and calculate GB postcodes we might be missing
600 foreach($aTokens as $sToken)
602 // Source of gb postcodes is now definitive - always use
603 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
605 if (substr($aData[1],-2,1) != ' ')
607 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
608 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
610 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
611 if ($aGBPostcodeLocation)
613 $aValidTokens[$sToken] = $aGBPostcodeLocation;
616 // US ZIP+4 codes - if there is no token,
617 // merge in the 5-digit ZIP code
618 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
620 if (isset($aValidTokens[$aData[1]]))
622 foreach($aValidTokens[$aData[1]] as $aToken)
624 if (!$aToken['class'])
626 if (isset($aValidTokens[$sToken]))
628 $aValidTokens[$sToken][] = $aToken;
632 $aValidTokens[$sToken] = array($aToken);
640 foreach($aTokens as $sToken)
642 // Unknown single word token with a number - assume it is a house number
643 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
645 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
649 // Any words that have failed completely?
652 // Start the search process
653 $aResultPlaceIDs = array();
656 Calculate all searches using aValidTokens i.e.
657 'Wodsworth Road, Sheffield' =>
661 0 1 (wodsworth)(road)
664 Score how good the search is so they can be ordered
666 foreach($aPhrases as $iPhrase => $sPhrase)
668 $aNewPhraseSearches = array();
669 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
670 else $sPhraseType = '';
672 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
674 // Too many permutations - too expensive
675 if ($iWordSet > 120) break;
677 $aWordsetSearches = $aSearches;
679 // Add all words from this wordset
680 foreach($aWordset as $iToken => $sToken)
682 //echo "<br><b>$sToken</b>";
683 $aNewWordsetSearches = array();
685 foreach($aWordsetSearches as $aCurrentSearch)
688 //var_dump($aCurrentSearch);
691 // If the token is valid
692 if (isset($aValidTokens[' '.$sToken]))
694 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
696 $aSearch = $aCurrentSearch;
697 $aSearch['iSearchRank']++;
698 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
700 if ($aSearch['sCountryCode'] === false)
702 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
703 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
704 // If reverse order is enabled, it may appear at the beginning as well.
705 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
706 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
708 $aSearch['iSearchRank'] += 5;
710 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
713 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
715 if ($aSearch['fLat'] === '')
717 $aSearch['fLat'] = $aSearchTerm['lat'];
718 $aSearch['fLon'] = $aSearchTerm['lon'];
719 $aSearch['fRadius'] = $aSearchTerm['radius'];
720 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
723 elseif ($sPhraseType == 'postalcode')
725 // 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
726 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
728 // If we already have a name try putting the postcode first
729 if (sizeof($aSearch['aName']))
731 $aNewSearch = $aSearch;
732 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
733 $aNewSearch['aName'] = array();
734 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
735 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
738 if (sizeof($aSearch['aName']))
740 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
742 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
746 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
747 $aSearch['iSearchRank'] += 1000; // skip;
752 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
753 //$aSearch['iNamePhrase'] = $iPhrase;
755 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
759 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
761 if ($aSearch['sHouseNumber'] === '')
763 $aSearch['sHouseNumber'] = $sToken;
764 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
766 // Fall back to not searching for this item (better than nothing)
767 $aSearch = $aCurrentSearch;
768 $aSearch['iSearchRank'] += 1;
769 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
773 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
775 if ($aSearch['sClass'] === '')
777 $aSearch['sOperator'] = $aSearchTerm['operator'];
778 $aSearch['sClass'] = $aSearchTerm['class'];
779 $aSearch['sType'] = $aSearchTerm['type'];
780 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
781 else $aSearch['sOperator'] = 'near'; // near = in for the moment
783 // Do we have a shortcut id?
784 if ($aSearch['sOperator'] == 'name')
786 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
787 if ($iAmenityID = $this->oDB->getOne($sSQL))
789 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
790 $aSearch['aName'][$iAmenityID] = $iAmenityID;
791 $aSearch['sClass'] = '';
792 $aSearch['sType'] = '';
795 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
798 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
800 if (sizeof($aSearch['aName']))
802 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
804 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
808 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
809 $aSearch['iSearchRank'] += 1000; // skip;
814 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
815 //$aSearch['iNamePhrase'] = $iPhrase;
817 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
821 if (isset($aValidTokens[$sToken]))
823 // Allow searching for a word - but at extra cost
824 foreach($aValidTokens[$sToken] as $aSearchTerm)
826 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
828 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
830 $aSearch = $aCurrentSearch;
831 $aSearch['iSearchRank'] += 1;
832 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
834 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
835 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
837 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
839 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
841 if (empty($aSearchTermToken['country_code'])
842 && empty($aSearchTermToken['lat'])
843 && empty($aSearchTermToken['class']))
845 $aSearch = $aCurrentSearch;
846 $aSearch['iSearchRank'] += 1;
847 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
848 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
854 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
855 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
859 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
861 $aSearch = $aCurrentSearch;
862 $aSearch['iSearchRank'] += 2;
863 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
864 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
865 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
867 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
868 $aSearch['iNamePhrase'] = $iPhrase;
869 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
876 // Allow skipping a word - but at EXTREAM cost
877 //$aSearch = $aCurrentSearch;
878 //$aSearch['iSearchRank']+=100;
879 //$aNewWordsetSearches[] = $aSearch;
883 usort($aNewWordsetSearches, 'bySearchRank');
884 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
886 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
888 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
889 usort($aNewPhraseSearches, 'bySearchRank');
891 $aSearchHash = array();
892 foreach($aNewPhraseSearches as $iSearch => $aSearch)
894 $sHash = serialize($aSearch);
895 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
896 else $aSearchHash[$sHash] = 1;
899 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
902 // Re-group the searches by their score, junk anything over 20 as just not worth trying
903 $aGroupedSearches = array();
904 foreach($aNewPhraseSearches as $aSearch)
906 if ($aSearch['iSearchRank'] < $this->iMaxRank)
908 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
909 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
912 ksort($aGroupedSearches);
915 $aSearches = array();
916 foreach($aGroupedSearches as $iScore => $aNewSearches)
918 $iSearchCount += sizeof($aNewSearches);
919 $aSearches = array_merge($aSearches, $aNewSearches);
920 if ($iSearchCount > 50) break;
923 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
930 // Re-group the searches by their score, junk anything over 20 as just not worth trying
931 $aGroupedSearches = array();
932 foreach($aSearches as $aSearch)
934 if ($aSearch['iSearchRank'] < $this->iMaxRank)
936 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
937 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
940 ksort($aGroupedSearches);
943 if (CONST_Debug) var_Dump($aGroupedSearches);
945 if ($this->bReverseInPlan)
947 $aCopyGroupedSearches = $aGroupedSearches;
948 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
950 foreach($aSearches as $iSearch => $aSearch)
952 if (sizeof($aSearch['aAddress']))
954 $iReverseItem = array_pop($aSearch['aAddress']);
955 if (isset($aPossibleMainWordIDs[$iReverseItem]))
957 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
958 $aSearch['aName'] = array($iReverseItem);
959 $aGroupedSearches[$iGroup][] = $aSearch;
961 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
962 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
968 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
970 $aCopyGroupedSearches = $aGroupedSearches;
971 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
973 foreach($aSearches as $iSearch => $aSearch)
975 $aReductionsList = array($aSearch['aAddress']);
976 $iSearchRank = $aSearch['iSearchRank'];
977 while(sizeof($aReductionsList) > 0)
980 if ($iSearchRank > iMaxRank) break 3;
981 $aNewReductionsList = array();
982 foreach($aReductionsList as $aReductionsWordList)
984 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
986 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
987 $aReverseSearch = $aSearch;
988 $aSearch['aAddress'] = $aReductionsWordListResult;
989 $aSearch['iSearchRank'] = $iSearchRank;
990 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
991 if (sizeof($aReductionsWordListResult) > 0)
993 $aNewReductionsList[] = $aReductionsWordListResult;
997 $aReductionsList = $aNewReductionsList;
1001 ksort($aGroupedSearches);
1004 // Filter out duplicate searches
1005 $aSearchHash = array();
1006 foreach($aGroupedSearches as $iGroup => $aSearches)
1008 foreach($aSearches as $iSearch => $aSearch)
1010 $sHash = serialize($aSearch);
1011 if (isset($aSearchHash[$sHash]))
1013 unset($aGroupedSearches[$iGroup][$iSearch]);
1014 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1018 $aSearchHash[$sHash] = 1;
1023 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1027 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1030 foreach($aSearches as $aSearch)
1034 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1035 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1037 // No location term?
1038 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1040 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1042 // Just looking for a country by code - look it up
1043 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1045 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1046 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1047 $sSQL .= " order by st_area(geometry) desc limit 1";
1048 if (CONST_Debug) var_dump($sSQL);
1049 $aPlaceIDs = $this->oDB->getCol($sSQL);
1054 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1055 if (!$aSearch['sClass']) continue;
1056 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1057 if ($this->oDB->getOne($sSQL))
1059 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1060 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1061 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
1062 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1063 if (sizeof($this->aExcludePlaceIDs))
1065 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1067 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1068 $sSQL .= " limit $this->iLimit";
1069 if (CONST_Debug) var_dump($sSQL);
1070 $aPlaceIDs = $this->oDB->getCol($sSQL);
1072 // If excluded place IDs are given, it is fair to assume that
1073 // there have been results in the small box, so no further
1074 // expansion in that case.
1075 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1077 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1078 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1079 $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
1080 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1081 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1082 $sSQL .= " limit $this->iLimit";
1083 if (CONST_Debug) var_dump($sSQL);
1084 $aPlaceIDs = $this->oDB->getCol($sSQL);
1089 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1090 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
1091 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1092 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1093 $sSQL .= " limit $this->iLimit";
1094 if (CONST_Debug) var_dump($sSQL);
1095 $aPlaceIDs = $this->oDB->getCol($sSQL);
1101 $aPlaceIDs = array();
1103 // First we need a position, either aName or fLat or both
1107 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1108 // they might be right - but they are just too darned expensive to run
1109 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1110 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1111 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1113 // For infrequent name terms disable index usage for address
1114 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1115 sizeof($aSearch['aName']) == 1 &&
1116 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1118 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1122 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1123 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1126 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1127 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1128 if ($aSearch['fLon'] && $aSearch['fLat'])
1130 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1131 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1133 if (sizeof($this->aExcludePlaceIDs))
1135 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1137 if ($sCountryCodesSQL)
1139 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1142 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
1143 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1145 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1146 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1147 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1148 $aOrder[] = "$sImportanceSQL DESC";
1149 if (sizeof($aSearch['aFullNameAddress']))
1151 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1154 if (sizeof($aTerms))
1156 $sSQL = "select place_id";
1157 $sSQL .= " from search_name";
1158 $sSQL .= " where ".join(' and ',$aTerms);
1159 $sSQL .= " order by ".join(', ',$aOrder);
1160 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1161 $sSQL .= " limit 50";
1162 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1163 $sSQL .= " limit 1";
1165 $sSQL .= " limit ".$this->iLimit;
1167 if (CONST_Debug) { var_dump($sSQL); }
1168 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1169 if (PEAR::IsError($aViewBoxPlaceIDs))
1171 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1173 //var_dump($aViewBoxPlaceIDs);
1174 // Did we have an viewbox matches?
1175 $aPlaceIDs = array();
1176 $bViewBoxMatch = false;
1177 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1179 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1180 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1181 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1182 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1183 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1186 //var_Dump($aPlaceIDs);
1189 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1191 $aRoadPlaceIDs = $aPlaceIDs;
1192 $sPlaceIDs = join(',',$aPlaceIDs);
1194 // Now they are indexed look for a house attached to a street we found
1195 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1196 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1197 if (sizeof($this->aExcludePlaceIDs))
1199 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1201 $sSQL .= " limit $this->iLimit";
1202 if (CONST_Debug) var_dump($sSQL);
1203 $aPlaceIDs = $this->oDB->getCol($sSQL);
1205 // If not try the aux fallback table
1206 if (!sizeof($aPlaceIDs))
1208 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1209 if (sizeof($this->aExcludePlaceIDs))
1211 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1213 //$sSQL .= " limit $this->iLimit";
1214 if (CONST_Debug) var_dump($sSQL);
1215 $aPlaceIDs = $this->oDB->getCol($sSQL);
1218 if (!sizeof($aPlaceIDs))
1220 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1221 if (sizeof($this->aExcludePlaceIDs))
1223 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1225 //$sSQL .= " limit $this->iLimit";
1226 if (CONST_Debug) var_dump($sSQL);
1227 $aPlaceIDs = $this->oDB->getCol($sSQL);
1230 // Fallback to the road
1231 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1233 $aPlaceIDs = $aRoadPlaceIDs;
1238 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1240 $sPlaceIDs = join(',',$aPlaceIDs);
1241 $aClassPlaceIDs = array();
1243 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1245 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1246 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1247 $sSQL .= " and linked_place_id is null";
1248 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1249 $sSQL .= " order by rank_search asc limit $this->iLimit";
1250 if (CONST_Debug) var_dump($sSQL);
1251 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1254 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1256 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1257 $bCacheTable = $this->oDB->getOne($sSQL);
1259 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1261 if (CONST_Debug) var_dump($sSQL);
1262 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1264 // For state / country level searches the normal radius search doesn't work very well
1265 $sPlaceGeom = false;
1266 if ($this->iMaxRank < 9 && $bCacheTable)
1268 // Try and get a polygon to search in instead
1269 $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";
1270 if (CONST_Debug) var_dump($sSQL);
1271 $sPlaceGeom = $this->oDB->getOne($sSQL);
1280 $this->iMaxRank += 5;
1281 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1282 if (CONST_Debug) var_dump($sSQL);
1283 $aPlaceIDs = $this->oDB->getCol($sSQL);
1284 $sPlaceIDs = join(',',$aPlaceIDs);
1287 if ($sPlaceIDs || $sPlaceGeom)
1293 // More efficient - can make the range bigger
1297 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1298 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1299 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1301 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1302 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1305 $sSQL .= ",placex as f where ";
1306 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1311 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1313 if (sizeof($this->aExcludePlaceIDs))
1315 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1317 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1318 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1319 if ($iOffset) $sSQL .= " offset $iOffset";
1320 $sSQL .= " limit $this->iLimit";
1321 if (CONST_Debug) var_dump($sSQL);
1322 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1326 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1329 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1330 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1332 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1333 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1334 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1335 if (sizeof($this->aExcludePlaceIDs))
1337 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1339 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1340 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1341 if ($iOffset) $sSQL .= " offset $iOffset";
1342 $sSQL .= " limit $this->iLimit";
1343 if (CONST_Debug) var_dump($sSQL);
1344 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1349 $aPlaceIDs = $aClassPlaceIDs;
1355 if (PEAR::IsError($aPlaceIDs))
1357 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1360 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1362 foreach($aPlaceIDs as $iPlaceID)
1364 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1366 if ($iQueryLoop > 20) break;
1369 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1371 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1372 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1373 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1374 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1375 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1376 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1377 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1378 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1380 if (CONST_Debug) var_dump($sSQL);
1381 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1385 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1386 if ($iGroupLoop > 4) break;
1387 if ($iQueryLoop > 30) break;
1390 // Did we find anything?
1391 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1393 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1399 // Just interpret as a reverse geocode
1400 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1402 $aSearchResults = $this->getDetails(array($iPlaceID));
1404 $aSearchResults = array();
1408 if (!sizeof($aSearchResults))
1410 if ($this->bFallback)
1412 if ($this->fallbackStructuredQuery())
1414 return $this->lookup();
1421 $aClassType = getClassTypesWithImportance();
1422 $aRecheckWords = preg_split('/\b/u',$sQuery);
1423 foreach($aRecheckWords as $i => $sWord)
1425 if (!$sWord) unset($aRecheckWords[$i]);
1428 foreach($aSearchResults as $iResNum => $aResult)
1430 if (CONST_Search_AreaPolygons)
1432 // Get the bounding box and outline polygon
1433 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1434 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1435 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1436 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1437 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1438 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1439 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1440 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1441 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1442 $aPointPolygon = $this->oDB->getRow($sSQL);
1443 if (PEAR::IsError($aPointPolygon))
1445 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1448 if ($aPointPolygon['place_id'])
1450 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1451 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1452 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1453 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1455 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1457 $aResult['lat'] = $aPointPolygon['centrelat'];
1458 $aResult['lon'] = $aPointPolygon['centrelon'];
1461 if ($this->bIncludePolygonAsPoints)
1463 // Translate geometary string to point array
1464 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1466 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1468 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1470 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1472 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1475 $iSteps = ($fRadius * 40000)^2;
1476 $fStepSize = (2*pi())/$iSteps;
1477 $aPolyPoints = array();
1478 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1480 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1482 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1483 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1484 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1485 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1489 // Output data suitable for display (points and a bounding box)
1490 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1492 $aResult['aPolyPoints'] = array();
1493 foreach($aPolyPoints as $aPoint)
1495 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1498 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1502 if ($aResult['extra_place'] == 'city')
1504 $aResult['class'] = 'place';
1505 $aResult['type'] = 'city';
1506 $aResult['rank_search'] = 16;
1509 if (!isset($aResult['aBoundingBox']))
1512 $fDiameter = 0.0001;
1514 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1515 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1517 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1519 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1520 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1522 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1524 $fRadius = $fDiameter / 2;
1526 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1527 $fStepSize = (2*pi())/$iSteps;
1528 $aPolyPoints = array();
1529 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1531 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1533 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1534 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1535 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1536 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1538 // Output data suitable for display (points and a bounding box)
1539 if ($this->bIncludePolygonAsPoints)
1541 $aResult['aPolyPoints'] = array();
1542 foreach($aPolyPoints as $aPoint)
1544 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1547 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1550 // Is there an icon set for this type of result?
1551 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1552 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1554 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1557 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1558 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1560 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1562 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1563 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1565 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1568 if ($this->bIncludeAddressDetails)
1570 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1571 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1573 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1577 // Adjust importance for the number of exact string matches in the result
1578 $aResult['importance'] = max(0.001,$aResult['importance']);
1580 $sAddress = $aResult['langaddress'];
1581 foreach($aRecheckWords as $i => $sWord)
1583 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1586 $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
1588 $aResult['name'] = $aResult['langaddress'];
1589 $aResult['foundorder'] = -$aResult['addressimportance'];
1590 $aSearchResults[$iResNum] = $aResult;
1592 uasort($aSearchResults, 'byImportance');
1594 $aOSMIDDone = array();
1595 $aClassTypeNameDone = array();
1596 $aToFilter = $aSearchResults;
1597 $aSearchResults = array();
1600 foreach($aToFilter as $iResNum => $aResult)
1602 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1603 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1606 $fLat = $aResult['lat'];
1607 $fLon = $aResult['lon'];
1608 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1611 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1612 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1614 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1615 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1616 $aSearchResults[] = $aResult;
1619 // Absolute limit on number of results
1620 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1623 return $aSearchResults;
1632 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1634 $aPoints = explode(',',$_GET['route']);
1635 if (sizeof($aPoints) % 2 != 0)
1637 userError("Uneven number of points");
1640 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1641 $fPrevCoord = false;