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();
38 protected $sAllowedTypesSQLList = false;
40 protected $sQuery = false;
41 protected $aStructuredQuery = false;
43 function Geocode(&$oDB)
48 function setLanguagePreference($aLangPref)
50 $this->aLangPrefOrder = $aLangPref;
53 function setIncludeAddressDetails($bAddressDetails = true)
55 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
58 function getIncludeAddressDetails()
60 return $this->bIncludeAddressDetails;
63 function setIncludePolygonAsPoints($b = true)
65 $this->bIncludePolygonAsPoints = $b;
68 function getIncludePolygonAsPoints()
70 return $this->bIncludePolygonAsPoints;
73 function setIncludePolygonAsText($b = true)
75 $this->bIncludePolygonAsText = $b;
78 function getIncludePolygonAsText()
80 return $this->bIncludePolygonAsText;
83 function setIncludePolygonAsGeoJSON($b = true)
85 $this->bIncludePolygonAsGeoJSON = $b;
88 function setIncludePolygonAsKML($b = true)
90 $this->bIncludePolygonAsKML = $b;
93 function setIncludePolygonAsSVG($b = true)
95 $this->bIncludePolygonAsSVG = $b;
98 function setDeDupe($bDeDupe = true)
100 $this->bDeDupe = (bool)$bDeDupe;
103 function setLimit($iLimit = 10)
105 if ($iLimit > 50) $iLimit = 50;
106 if ($iLimit < 1) $iLimit = 1;
108 $this->iFinalLimit = $iLimit;
109 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
112 function setOffset($iOffset = 0)
114 $this->iOffset = $iOffset;
117 function setExcludedPlaceIDs($a)
119 // TODO: force to int
120 $this->aExcludePlaceIDs = $a;
123 function getExcludedPlaceIDs()
125 return $this->aExcludePlaceIDs;
128 function setBounded($bBoundedSearch = true)
130 $this->bBoundedSearch = (bool)$bBoundedSearch;
133 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
135 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
138 function getViewBoxString()
140 if (!$this->aViewBox) return null;
141 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
144 function setRoute($aRoutePoints)
146 $this->aRoutePoints = $aRoutePoints;
149 function setFeatureType($sFeatureType)
151 switch($sFeatureType)
154 $this->setRankRange(4, 4);
157 $this->setRankRange(8, 8);
160 $this->setRankRange(14, 16);
163 $this->setRankRange(8, 20);
168 function setRankRange($iMin, $iMax)
170 $this->iMinAddressRank = (int)$iMin;
171 $this->iMaxAddressRank = (int)$iMax;
174 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
176 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
179 function setCountryCodesList($aCountryCodes)
181 $this->aCountryCodes = $aCountryCodes;
184 function setQuery($sQueryString)
186 $this->sQuery = $sQueryString;
187 $this->aStructuredQuery = false;
190 function getQueryString()
192 return $this->sQuery;
195 function loadStructuredAddressElement(&$aStructuredQuery, &$iMinAddressRank, &$iMaxAddressRank, &$aAddressRankList, $sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
197 $sValue = trim($sValue);
198 if (!$sValue) return false;
199 $aStructuredQuery[$sKey] = $sValue;
200 if ($iMinAddressRank == 0 && $iMaxAddressRank == 30)
202 $iMinAddressRank = $iNewMinAddressRank;
203 $iMaxAddressRank = $iNewMaxAddressRank;
205 if ($aItemListValues) $aAddressRankList = array_merge($aAddressRankList, $aItemListValues);
209 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
211 $this->sQuery = false;
213 $this->aStructuredQuery = array();
214 $this->sAllowedTypesSQLList = '';
216 $this->loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sAmentiy, 'amenity', 26, 30, false);
217 $this->loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sStreet, 'street', 26, 30, false);
218 $this->loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCity, 'city', 14, 24, false);
219 $this->loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCounty, 'county', 9, 13, false);
220 $this->loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sState, 'state', 8, 8, false);
221 $this->loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCountry, 'country', 4, 4, false);
222 $this->loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sPostalCode, 'postalcode' , 5, 11, array(5, 11));
224 if (sizeof($this->aStructuredQuery) > 0)
226 $this->sQuery = join(', ', $this->aStructuredQuery);
227 if ($this->iMaxAddressRank < 30)
229 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
235 function getDetails($aPlaceIDs)
237 if (sizeof($aPlaceIDs) == 0) return array();
239 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
241 // Get the details for display (is this a redundant extra step?)
242 $sPlaceIDs = join(',',$aPlaceIDs);
244 $sImportanceSQL = '';
245 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
246 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
248 $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,";
249 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
250 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
251 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
252 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
253 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
254 $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, ";
255 $sSQL .= "(extratags->'place') as extra_place ";
256 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
257 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
258 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
259 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
261 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
262 $sSQL .= "and linked_place_id is null ";
263 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
264 if (!$this->bDeDupe) $sSQL .= ",place_id";
265 $sSQL .= ",langaddress ";
266 $sSQL .= ",placename ";
268 $sSQL .= ",extratags->'place' ";
270 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
273 $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,";
274 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
275 $sSQL .= "null as placename,";
276 $sSQL .= "null as ref,";
277 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
278 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
279 $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, ";
280 $sSQL .= "null as extra_place ";
281 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
282 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
283 $sSQL .= "group by place_id";
284 if (!$this->bDeDupe) $sSQL .= ",place_id ";
287 $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,";
288 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
289 $sSQL .= "null as placename,";
290 $sSQL .= "null as ref,";
291 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
292 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
293 $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, ";
294 $sSQL .= "null as extra_place ";
295 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
296 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
297 $sSQL .= "group by place_id";
298 if (!$this->bDeDupe) $sSQL .= ",place_id";
299 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
303 $sSQL .= " order by importance desc";
304 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
305 $aSearchResults = $this->oDB->getAll($sSQL);
307 if (PEAR::IsError($aSearchResults))
309 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
312 return $aSearchResults;
317 if (!$this->sQuery && !$this->aStructuredQuery) return false;
319 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
321 $sCountryCodesSQL = false;
322 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
324 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
327 // Hack to make it handle "new york, ny" (and variants) correctly
328 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
330 // Conflicts between US state abreviations and various words for 'the' in different languages
331 if (isset($this->aLangPrefOrder['name:en']))
333 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
334 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
335 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
340 $bBoundingBoxSearch = false;
343 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
344 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
345 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
346 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
347 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
348 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
350 $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)";
351 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
352 $bBoundingBoxSearch = $this->bBoundedSearch;
356 if ($this->aRoutePoints)
358 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
360 foreach($this->aRouteaPoints as $aPoint)
362 if (!$bFirst) $sViewboxCentreSQL .= ",";
363 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
365 $sViewboxCentreSQL .= ")'::geometry,4326)";
367 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
368 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
369 if (PEAR::isError($this->sViewboxSmallSQL))
371 failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
373 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
375 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
376 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
377 if (PEAR::isError($this->sViewboxLargeSQL))
379 failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
381 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
382 $bBoundingBoxSearch = $this->bBoundedSearch;
385 // Do we have anything that looks like a lat/lon pair?
386 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
388 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
389 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
390 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
392 $this->setNearPoint(array($fQueryLat, $fQueryLon));
393 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
396 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
398 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
399 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
400 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
402 $this->setNearPoint(array($fQueryLat, $fQueryLon));
403 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
406 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
408 $fQueryLat = $aData[2];
409 $fQueryLon = $aData[3];
410 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
412 $this->setNearPoint(array($fQueryLat, $fQueryLon));
413 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
417 $aSearchResults = array();
418 if ($sQuery || $this->aStructuredQuery)
420 // Start with a blank search
422 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
423 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
424 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
427 // Do we have a radius search?
428 $sNearPointSQL = false;
429 if ($this->aNearPoint)
431 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
432 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
433 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
434 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
437 // Any 'special' terms in the search?
438 $bSpecialTerms = false;
439 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
440 $aSpecialTerms = array();
441 foreach($aSpecialTermsRaw as $aSpecialTerm)
443 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
444 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
447 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
448 $aSpecialTerms = array();
449 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
451 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
452 unset($aStructuredQuery['amenity']);
454 foreach($aSpecialTermsRaw as $aSpecialTerm)
456 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
457 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
458 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
459 $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';
460 if (CONST_Debug) var_Dump($sSQL);
461 $aSearchWords = $this->oDB->getAll($sSQL);
462 $aNewSearches = array();
463 foreach($aSearches as $aSearch)
465 foreach($aSearchWords as $aSearchTerm)
467 $aNewSearch = $aSearch;
468 if ($aSearchTerm['country_code'])
470 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
471 $aNewSearches[] = $aNewSearch;
472 $bSpecialTerms = true;
474 if ($aSearchTerm['class'])
476 $aNewSearch['sClass'] = $aSearchTerm['class'];
477 $aNewSearch['sType'] = $aSearchTerm['type'];
478 $aNewSearches[] = $aNewSearch;
479 $bSpecialTerms = true;
483 $aSearches = $aNewSearches;
486 // Split query into phrases
487 // Commas are used to reduce the search space by indicating where phrases split
488 if ($this->aStructuredQuery)
490 $aPhrases = $this->aStructuredQuery;
491 $bStructuredPhrases = true;
495 $aPhrases = explode(',',$sQuery);
496 $bStructuredPhrases = false;
499 // Convert each phrase to standard form
500 // Create a list of standard words
501 // Get all 'sets' of words
502 // Generate a complete list of all
504 foreach($aPhrases as $iPhrase => $sPhrase)
506 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
507 if (PEAR::isError($aPhrase))
509 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
510 if (CONST_Debug) var_dump($aPhrase);
513 if (trim($aPhrase['string']))
515 $aPhrases[$iPhrase] = $aPhrase;
516 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
517 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
518 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
522 unset($aPhrases[$iPhrase]);
526 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
527 $aPhraseTypes = array_keys($aPhrases);
528 $aPhrases = array_values($aPhrases);
530 if (sizeof($aTokens))
532 // Check which tokens we have, get the ID numbers
533 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
534 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
536 if (CONST_Debug) var_Dump($sSQL);
538 $aValidTokens = array();
539 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
540 else $aDatabaseWords = array();
541 if (PEAR::IsError($aDatabaseWords))
543 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
545 $aPossibleMainWordIDs = array();
546 $aWordFrequencyScores = array();
547 foreach($aDatabaseWords as $aToken)
549 // Very special case - require 2 letter country param to match the country code found
550 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
551 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
556 if (isset($aValidTokens[$aToken['word_token']]))
558 $aValidTokens[$aToken['word_token']][] = $aToken;
562 $aValidTokens[$aToken['word_token']] = array($aToken);
564 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
565 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
567 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
569 // Try and calculate GB postcodes we might be missing
570 foreach($aTokens as $sToken)
572 // Source of gb postcodes is now definitive - always use
573 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
575 if (substr($aData[1],-2,1) != ' ')
577 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
578 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
580 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
581 if ($aGBPostcodeLocation)
583 $aValidTokens[$sToken] = $aGBPostcodeLocation;
586 // US ZIP+4 codes - if there is no token,
587 // merge in the 5-digit ZIP code
588 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
590 if (isset($aValidTokens[$aData[1]]))
592 foreach($aValidTokens[$aData[1]] as $aToken)
594 if (!$aToken['class'])
596 if (isset($aValidTokens[$sToken]))
598 $aValidTokens[$sToken][] = $aToken;
602 $aValidTokens[$sToken] = array($aToken);
610 foreach($aTokens as $sToken)
612 // Unknown single word token with a number - assume it is a house number
613 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
615 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
619 // Any words that have failed completely?
622 // Start the search process
623 $aResultPlaceIDs = array();
626 Calculate all searches using aValidTokens i.e.
627 'Wodsworth Road, Sheffield' =>
631 0 1 (wodsworth)(road)
634 Score how good the search is so they can be ordered
636 foreach($aPhrases as $iPhrase => $sPhrase)
638 $aNewPhraseSearches = array();
639 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
640 else $sPhraseType = '';
642 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
644 // Too many permutations - too expensive
645 if ($iWordSet > 120) break;
647 $aWordsetSearches = $aSearches;
649 // Add all words from this wordset
650 foreach($aWordset as $iToken => $sToken)
652 //echo "<br><b>$sToken</b>";
653 $aNewWordsetSearches = array();
655 foreach($aWordsetSearches as $aCurrentSearch)
658 //var_dump($aCurrentSearch);
661 // If the token is valid
662 if (isset($aValidTokens[' '.$sToken]))
664 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
666 $aSearch = $aCurrentSearch;
667 $aSearch['iSearchRank']++;
668 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
670 if ($aSearch['sCountryCode'] === false)
672 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
673 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
674 // If reverse order is enabled, it may appear at the beginning as well.
675 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
676 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
678 $aSearch['iSearchRank'] += 5;
680 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
683 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
685 if ($aSearch['fLat'] === '')
687 $aSearch['fLat'] = $aSearchTerm['lat'];
688 $aSearch['fLon'] = $aSearchTerm['lon'];
689 $aSearch['fRadius'] = $aSearchTerm['radius'];
690 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
693 elseif ($sPhraseType == 'postalcode')
695 // 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
696 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
698 // If we already have a name try putting the postcode first
699 if (sizeof($aSearch['aName']))
701 $aNewSearch = $aSearch;
702 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
703 $aNewSearch['aName'] = array();
704 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
705 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
708 if (sizeof($aSearch['aName']))
710 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
712 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
716 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
717 $aSearch['iSearchRank'] += 1000; // skip;
722 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
723 //$aSearch['iNamePhrase'] = $iPhrase;
725 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
729 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
731 if ($aSearch['sHouseNumber'] === '')
733 $aSearch['sHouseNumber'] = $sToken;
734 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
736 // Fall back to not searching for this item (better than nothing)
737 $aSearch = $aCurrentSearch;
738 $aSearch['iSearchRank'] += 1;
739 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
743 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
745 if ($aSearch['sClass'] === '')
747 $aSearch['sOperator'] = $aSearchTerm['operator'];
748 $aSearch['sClass'] = $aSearchTerm['class'];
749 $aSearch['sType'] = $aSearchTerm['type'];
750 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
751 else $aSearch['sOperator'] = 'near'; // near = in for the moment
752 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
754 // Do we have a shortcut id?
755 if ($aSearch['sOperator'] == 'name')
757 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
758 if ($iAmenityID = $this->oDB->getOne($sSQL))
760 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
761 $aSearch['aName'][$iAmenityID] = $iAmenityID;
762 $aSearch['sClass'] = '';
763 $aSearch['sType'] = '';
766 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
769 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
771 if (sizeof($aSearch['aName']))
773 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
775 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
779 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
780 $aSearch['iSearchRank'] += 1000; // skip;
785 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
786 //$aSearch['iNamePhrase'] = $iPhrase;
788 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
792 if (isset($aValidTokens[$sToken]))
794 // Allow searching for a word - but at extra cost
795 foreach($aValidTokens[$sToken] as $aSearchTerm)
797 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
799 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
801 $aSearch = $aCurrentSearch;
802 $aSearch['iSearchRank'] += 1;
803 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
805 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
806 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
808 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
810 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
812 if (empty($aSearchTermToken['country_code'])
813 && empty($aSearchTermToken['lat'])
814 && empty($aSearchTermToken['class']))
816 $aSearch = $aCurrentSearch;
817 $aSearch['iSearchRank'] += 1;
818 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
819 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
825 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
826 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
830 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
832 $aSearch = $aCurrentSearch;
833 $aSearch['iSearchRank'] += 2;
834 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
835 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
836 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
838 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
839 $aSearch['iNamePhrase'] = $iPhrase;
840 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
847 // Allow skipping a word - but at EXTREAM cost
848 //$aSearch = $aCurrentSearch;
849 //$aSearch['iSearchRank']+=100;
850 //$aNewWordsetSearches[] = $aSearch;
854 usort($aNewWordsetSearches, 'bySearchRank');
855 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
857 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
859 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
860 usort($aNewPhraseSearches, 'bySearchRank');
862 $aSearchHash = array();
863 foreach($aNewPhraseSearches as $iSearch => $aSearch)
865 $sHash = serialize($aSearch);
866 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
867 else $aSearchHash[$sHash] = 1;
870 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
873 // Re-group the searches by their score, junk anything over 20 as just not worth trying
874 $aGroupedSearches = array();
875 foreach($aNewPhraseSearches as $aSearch)
877 if ($aSearch['iSearchRank'] < $this->iMaxRank)
879 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
880 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
883 ksort($aGroupedSearches);
886 $aSearches = array();
887 foreach($aGroupedSearches as $iScore => $aNewSearches)
889 $iSearchCount += sizeof($aNewSearches);
890 $aSearches = array_merge($aSearches, $aNewSearches);
891 if ($iSearchCount > 50) break;
894 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
901 // Re-group the searches by their score, junk anything over 20 as just not worth trying
902 $aGroupedSearches = array();
903 foreach($aSearches as $aSearch)
905 if ($aSearch['iSearchRank'] < $this->iMaxRank)
907 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
908 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
911 ksort($aGroupedSearches);
914 if (CONST_Debug) var_Dump($aGroupedSearches);
916 if ($this->bReverseInPlan)
918 $aCopyGroupedSearches = $aGroupedSearches;
919 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
921 foreach($aSearches as $iSearch => $aSearch)
923 if (sizeof($aSearch['aAddress']))
925 $iReverseItem = array_pop($aSearch['aAddress']);
926 if (isset($aPossibleMainWordIDs[$iReverseItem]))
928 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
929 $aSearch['aName'] = array($iReverseItem);
930 $aGroupedSearches[$iGroup][] = $aSearch;
932 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
933 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
939 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
941 $aCopyGroupedSearches = $aGroupedSearches;
942 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
944 foreach($aSearches as $iSearch => $aSearch)
946 $aReductionsList = array($aSearch['aAddress']);
947 $iSearchRank = $aSearch['iSearchRank'];
948 while(sizeof($aReductionsList) > 0)
951 if ($iSearchRank > iMaxRank) break 3;
952 $aNewReductionsList = array();
953 foreach($aReductionsList as $aReductionsWordList)
955 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
957 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
958 $aReverseSearch = $aSearch;
959 $aSearch['aAddress'] = $aReductionsWordListResult;
960 $aSearch['iSearchRank'] = $iSearchRank;
961 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
962 if (sizeof($aReductionsWordListResult) > 0)
964 $aNewReductionsList[] = $aReductionsWordListResult;
968 $aReductionsList = $aNewReductionsList;
972 ksort($aGroupedSearches);
975 // Filter out duplicate searches
976 $aSearchHash = array();
977 foreach($aGroupedSearches as $iGroup => $aSearches)
979 foreach($aSearches as $iSearch => $aSearch)
981 $sHash = serialize($aSearch);
982 if (isset($aSearchHash[$sHash]))
984 unset($aGroupedSearches[$iGroup][$iSearch]);
985 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
989 $aSearchHash[$sHash] = 1;
994 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
998 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1001 foreach($aSearches as $aSearch)
1005 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1006 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1008 // No location term?
1009 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1011 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1013 // Just looking for a country by code - look it up
1014 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1016 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1017 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1018 $sSQL .= " order by st_area(geometry) desc limit 1";
1019 if (CONST_Debug) var_dump($sSQL);
1020 $aPlaceIDs = $this->oDB->getCol($sSQL);
1025 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1026 if (!$aSearch['sClass']) continue;
1027 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1028 if ($this->oDB->getOne($sSQL))
1030 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1031 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1032 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1033 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1034 if (sizeof($this->aExcludePlaceIDs))
1036 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1038 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1039 $sSQL .= " limit $this->iLimit";
1040 if (CONST_Debug) var_dump($sSQL);
1041 $aPlaceIDs = $this->oDB->getCol($sSQL);
1043 // If excluded place IDs are given, it is fair to assume that
1044 // there have been results in the small box, so no further
1045 // expansion in that case.
1046 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1048 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1049 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1050 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1051 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1052 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1053 $sSQL .= " limit $this->iLimit";
1054 if (CONST_Debug) var_dump($sSQL);
1055 $aPlaceIDs = $this->oDB->getCol($sSQL);
1060 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1061 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1062 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1063 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1064 $sSQL .= " limit $this->iLimit";
1065 if (CONST_Debug) var_dump($sSQL);
1066 $aPlaceIDs = $this->oDB->getCol($sSQL);
1072 $aPlaceIDs = array();
1074 // First we need a position, either aName or fLat or both
1078 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1079 // they might be right - but they are just too darned expensive to run
1080 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1081 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1082 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1084 // For infrequent name terms disable index usage for address
1085 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1086 sizeof($aSearch['aName']) == 1 &&
1087 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1089 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1093 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1094 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1097 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1098 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1099 if ($aSearch['fLon'] && $aSearch['fLat'])
1101 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1102 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1104 if (sizeof($this->aExcludePlaceIDs))
1106 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1108 if ($sCountryCodesSQL)
1110 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1113 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1114 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1116 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1117 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1118 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1119 $aOrder[] = "$sImportanceSQL DESC";
1120 if (sizeof($aSearch['aFullNameAddress']))
1122 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1125 if (sizeof($aTerms))
1127 $sSQL = "select place_id";
1128 $sSQL .= " from search_name";
1129 $sSQL .= " where ".join(' and ',$aTerms);
1130 $sSQL .= " order by ".join(', ',$aOrder);
1131 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1132 $sSQL .= " limit 50";
1133 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1134 $sSQL .= " limit 1";
1136 $sSQL .= " limit ".$this->iLimit;
1138 if (CONST_Debug) { var_dump($sSQL); }
1139 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1140 if (PEAR::IsError($aViewBoxPlaceIDs))
1142 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1144 //var_dump($aViewBoxPlaceIDs);
1145 // Did we have an viewbox matches?
1146 $aPlaceIDs = array();
1147 $bViewBoxMatch = false;
1148 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1150 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1151 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1152 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1153 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1154 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1157 //var_Dump($aPlaceIDs);
1160 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1162 $aRoadPlaceIDs = $aPlaceIDs;
1163 $sPlaceIDs = join(',',$aPlaceIDs);
1165 // Now they are indexed look for a house attached to a street we found
1166 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1167 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1168 if (sizeof($this->aExcludePlaceIDs))
1170 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1172 $sSQL .= " limit $this->iLimit";
1173 if (CONST_Debug) var_dump($sSQL);
1174 $aPlaceIDs = $this->oDB->getCol($sSQL);
1176 // If not try the aux fallback table
1178 if (!sizeof($aPlaceIDs))
1180 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1181 if (sizeof($this->aExcludePlaceIDs))
1183 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1185 //$sSQL .= " limit $this->iLimit";
1186 if (CONST_Debug) var_dump($sSQL);
1187 $aPlaceIDs = $this->oDB->getCol($sSQL);
1191 if (!sizeof($aPlaceIDs))
1193 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1194 if (sizeof($this->aExcludePlaceIDs))
1196 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1198 //$sSQL .= " limit $this->iLimit";
1199 if (CONST_Debug) var_dump($sSQL);
1200 $aPlaceIDs = $this->oDB->getCol($sSQL);
1203 // Fallback to the road
1204 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1206 $aPlaceIDs = $aRoadPlaceIDs;
1211 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1213 $sPlaceIDs = join(',',$aPlaceIDs);
1214 $aClassPlaceIDs = array();
1216 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1218 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1219 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1220 $sSQL .= " and linked_place_id is null";
1221 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1222 $sSQL .= " order by rank_search asc limit $this->iLimit";
1223 if (CONST_Debug) var_dump($sSQL);
1224 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1227 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1229 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1230 $bCacheTable = $this->oDB->getOne($sSQL);
1232 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1234 if (CONST_Debug) var_dump($sSQL);
1235 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1237 // For state / country level searches the normal radius search doesn't work very well
1238 $sPlaceGeom = false;
1239 if ($this->iMaxRank < 9 && $bCacheTable)
1241 // Try and get a polygon to search in instead
1242 $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";
1243 if (CONST_Debug) var_dump($sSQL);
1244 $sPlaceGeom = $this->oDB->getOne($sSQL);
1253 $this->iMaxRank += 5;
1254 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1255 if (CONST_Debug) var_dump($sSQL);
1256 $aPlaceIDs = $this->oDB->getCol($sSQL);
1257 $sPlaceIDs = join(',',$aPlaceIDs);
1260 if ($sPlaceIDs || $sPlaceGeom)
1266 // More efficient - can make the range bigger
1270 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1271 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1272 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1274 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1275 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1278 $sSQL .= ",placex as f where ";
1279 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1284 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1286 if (sizeof($this->aExcludePlaceIDs))
1288 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1290 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1291 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1292 if ($iOffset) $sSQL .= " offset $iOffset";
1293 $sSQL .= " limit $this->iLimit";
1294 if (CONST_Debug) var_dump($sSQL);
1295 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1299 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1302 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1303 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1305 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1306 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1307 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1308 if (sizeof($this->aExcludePlaceIDs))
1310 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1312 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1313 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1314 if ($iOffset) $sSQL .= " offset $iOffset";
1315 $sSQL .= " limit $this->iLimit";
1316 if (CONST_Debug) var_dump($sSQL);
1317 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1322 $aPlaceIDs = $aClassPlaceIDs;
1328 if (PEAR::IsError($aPlaceIDs))
1330 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1333 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1335 foreach($aPlaceIDs as $iPlaceID)
1337 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1339 if ($iQueryLoop > 20) break;
1342 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1344 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1345 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1346 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1347 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1348 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1349 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1350 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1351 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1353 if (CONST_Debug) var_dump($sSQL);
1354 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1358 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1359 if ($iGroupLoop > 4) break;
1360 if ($iQueryLoop > 30) break;
1363 // Did we find anything?
1364 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1366 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1372 // Just interpret as a reverse geocode
1373 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1375 $aSearchResults = $this->getDetails(array($iPlaceID));
1377 $aSearchResults = array();
1381 if (!sizeof($aSearchResults))
1386 $aClassType = getClassTypesWithImportance();
1387 $aRecheckWords = preg_split('/\b/u',$sQuery);
1388 foreach($aRecheckWords as $i => $sWord)
1390 if (!$sWord) unset($aRecheckWords[$i]);
1393 foreach($aSearchResults as $iResNum => $aResult)
1395 if (CONST_Search_AreaPolygons)
1397 // Get the bounding box and outline polygon
1398 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1399 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1400 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1401 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1402 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1403 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1404 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1405 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1406 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1407 $aPointPolygon = $this->oDB->getRow($sSQL);
1408 if (PEAR::IsError($aPointPolygon))
1410 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1413 if ($aPointPolygon['place_id'])
1415 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1416 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1417 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1418 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1420 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1422 $aResult['lat'] = $aPointPolygon['centrelat'];
1423 $aResult['lon'] = $aPointPolygon['centrelon'];
1426 if ($this->bIncludePolygonAsPoints)
1428 // Translate geometary string to point array
1429 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1431 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1433 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1435 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1437 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1440 $iSteps = ($fRadius * 40000)^2;
1441 $fStepSize = (2*pi())/$iSteps;
1442 $aPolyPoints = array();
1443 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1445 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1447 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1448 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1449 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1450 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1454 // Output data suitable for display (points and a bounding box)
1455 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1457 $aResult['aPolyPoints'] = array();
1458 foreach($aPolyPoints as $aPoint)
1460 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1463 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1467 if ($aResult['extra_place'] == 'city')
1469 $aResult['class'] = 'place';
1470 $aResult['type'] = 'city';
1471 $aResult['rank_search'] = 16;
1474 if (!isset($aResult['aBoundingBox']))
1477 $fDiameter = 0.0001;
1479 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1480 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1482 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1484 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1485 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1487 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1489 $fRadius = $fDiameter / 2;
1491 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1492 $fStepSize = (2*pi())/$iSteps;
1493 $aPolyPoints = array();
1494 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1496 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1498 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1499 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1500 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1501 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1503 // Output data suitable for display (points and a bounding box)
1504 if ($this->bIncludePolygonAsPoints)
1506 $aResult['aPolyPoints'] = array();
1507 foreach($aPolyPoints as $aPoint)
1509 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1512 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1515 // Is there an icon set for this type of result?
1516 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1517 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1519 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1522 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1523 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1525 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1527 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1528 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1530 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1533 if ($this->bIncludeAddressDetails)
1535 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1536 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1538 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1542 // Adjust importance for the number of exact string matches in the result
1543 $aResult['importance'] = max(0.001,$aResult['importance']);
1545 $sAddress = $aResult['langaddress'];
1546 foreach($aRecheckWords as $i => $sWord)
1548 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1551 $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
1553 $aResult['name'] = $aResult['langaddress'];
1554 $aResult['foundorder'] = -$aResult['addressimportance'];
1555 $aSearchResults[$iResNum] = $aResult;
1557 uasort($aSearchResults, 'byImportance');
1559 $aOSMIDDone = array();
1560 $aClassTypeNameDone = array();
1561 $aToFilter = $aSearchResults;
1562 $aSearchResults = array();
1565 foreach($aToFilter as $iResNum => $aResult)
1567 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1568 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1571 $fLat = $aResult['lat'];
1572 $fLon = $aResult['lon'];
1573 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1576 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1577 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1579 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1580 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1581 $aSearchResults[] = $aResult;
1584 // Absolute limit on number of results
1585 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1588 return $aSearchResults;
1597 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1599 $aPoints = explode(',',$_GET['route']);
1600 if (sizeof($aPoints) % 2 != 0)
1602 userError("Uneven number of points");
1605 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1606 $fPrevCoord = false;