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 setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
197 $this->sQuery = false;
199 $this->aStructuredQuery = array();
200 $this->sAllowedTypesSQLList = '';
202 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sAmentiy, 'amenity', 26, 30, false);
203 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sStreet, 'street', 26, 30, false);
204 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCity, 'city', 14, 24, false);
205 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCounty, 'county', 9, 13, false);
206 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sState, 'state', 8, 8, false);
207 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCountry, 'country', 4, 4, false);
208 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sPostalCode, 'postalcode' , 5, 11, array(5, 11));
210 if (sizeof($this->aStructuredQuery) > 0)
212 $this->sQuery = join(', ', $this->aStructuredQuery);
213 if ($this->iMaxAddressRank < 30)
215 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
221 function getDetails($aPlaceIDs)
223 if (sizeof($aPlaceIDs) == 0) return array();
225 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
227 // Get the details for display (is this a redundant extra step?)
228 $sPlaceIDs = join(',',$aPlaceIDs);
230 $sImportanceSQL = '';
231 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
232 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
234 $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,";
235 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
236 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
237 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
238 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
239 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
240 $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, ";
241 $sSQL .= "(extratags->'place') as extra_place ";
242 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
243 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
244 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
245 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
247 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
248 $sSQL .= "and linked_place_id is null ";
249 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
250 if (!$this->bDeDupe) $sSQL .= ",place_id";
251 $sSQL .= ",langaddress ";
252 $sSQL .= ",placename ";
254 $sSQL .= ",extratags->'place' ";
256 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
259 $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,";
260 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
261 $sSQL .= "null as placename,";
262 $sSQL .= "null as ref,";
263 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
264 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
265 $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, ";
266 $sSQL .= "null as extra_place ";
267 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
268 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
269 $sSQL .= "group by place_id";
270 if (!$this->bDeDupe) $sSQL .= ",place_id ";
273 $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,";
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.10 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_aux.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_aux 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";
285 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
289 $sSQL .= " order by importance desc";
290 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
291 $aSearchResults = $this->oDB->getAll($sSQL);
293 if (PEAR::IsError($aSearchResults))
295 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
298 return $aSearchResults;
303 if (!$this->sQuery && !$this->aStructuredQuery) return false;
305 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
307 $sCountryCodesSQL = false;
308 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
310 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
313 // Hack to make it handle "new york, ny" (and variants) correctly
314 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
316 // Conflicts between US state abreviations and various words for 'the' in different languages
317 if (isset($this->aLangPrefOrder['name:en']))
319 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
320 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
321 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
326 $bBoundingBoxSearch = false;
329 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
330 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
331 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
332 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
333 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
334 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
336 $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)";
337 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
338 $bBoundingBoxSearch = $this->bBoundedSearch;
342 if ($this->aRoutePoints)
344 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
346 foreach($this->aRouteaPoints as $aPoint)
348 if (!$bFirst) $sViewboxCentreSQL .= ",";
349 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
351 $sViewboxCentreSQL .= ")'::geometry,4326)";
353 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
354 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
355 if (PEAR::isError($this->sViewboxSmallSQL))
357 failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
359 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
361 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
362 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
363 if (PEAR::isError($this->sViewboxLargeSQL))
365 failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
367 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
368 $bBoundingBoxSearch = $this->bBoundedSearch;
371 // Do we have anything that looks like a lat/lon pair?
372 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
374 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
375 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
376 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
378 $this->setNearPoint(array($fQueryLat, $fQueryLon));
379 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
382 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
384 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
385 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
386 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
388 $this->setNearPoint(array($fQueryLat, $fQueryLon));
389 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
392 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
394 $fQueryLat = $aData[2];
395 $fQueryLon = $aData[3];
396 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
398 $this->setNearPoint(array($fQueryLat, $fQueryLon));
399 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
403 $aSearchResults = array();
404 if ($sQuery || $this->aStructuredQuery)
406 // Start with a blank search
408 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
409 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
410 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
413 // Do we have a radius search?
414 $sNearPointSQL = false;
415 if ($this->aNearPoint)
417 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
418 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
419 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
420 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
423 // Any 'special' terms in the search?
424 $bSpecialTerms = false;
425 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
426 $aSpecialTerms = array();
427 foreach($aSpecialTermsRaw as $aSpecialTerm)
429 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
430 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
433 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
434 $aSpecialTerms = array();
435 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
437 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
438 unset($aStructuredQuery['amenity']);
440 foreach($aSpecialTermsRaw as $aSpecialTerm)
442 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
443 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
444 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
445 $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';
446 if (CONST_Debug) var_Dump($sSQL);
447 $aSearchWords = $this->oDB->getAll($sSQL);
448 $aNewSearches = array();
449 foreach($aSearches as $aSearch)
451 foreach($aSearchWords as $aSearchTerm)
453 $aNewSearch = $aSearch;
454 if ($aSearchTerm['country_code'])
456 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
457 $aNewSearches[] = $aNewSearch;
458 $bSpecialTerms = true;
460 if ($aSearchTerm['class'])
462 $aNewSearch['sClass'] = $aSearchTerm['class'];
463 $aNewSearch['sType'] = $aSearchTerm['type'];
464 $aNewSearches[] = $aNewSearch;
465 $bSpecialTerms = true;
469 $aSearches = $aNewSearches;
472 // Split query into phrases
473 // Commas are used to reduce the search space by indicating where phrases split
474 if ($this->aStructuredQuery)
476 $aPhrases = $this->aStructuredQuery;
477 $bStructuredPhrases = true;
481 $aPhrases = explode(',',$sQuery);
482 $bStructuredPhrases = false;
485 // Convert each phrase to standard form
486 // Create a list of standard words
487 // Get all 'sets' of words
488 // Generate a complete list of all
490 foreach($aPhrases as $iPhrase => $sPhrase)
492 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
493 if (PEAR::isError($aPhrase))
495 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
496 if (CONST_Debug) var_dump($aPhrase);
499 if (trim($aPhrase['string']))
501 $aPhrases[$iPhrase] = $aPhrase;
502 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
503 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
504 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
508 unset($aPhrases[$iPhrase]);
512 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
513 $aPhraseTypes = array_keys($aPhrases);
514 $aPhrases = array_values($aPhrases);
516 if (sizeof($aTokens))
518 // Check which tokens we have, get the ID numbers
519 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
520 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
522 if (CONST_Debug) var_Dump($sSQL);
524 $aValidTokens = array();
525 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
526 else $aDatabaseWords = array();
527 if (PEAR::IsError($aDatabaseWords))
529 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
531 $aPossibleMainWordIDs = array();
532 $aWordFrequencyScores = array();
533 foreach($aDatabaseWords as $aToken)
535 // Very special case - require 2 letter country param to match the country code found
536 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
537 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
542 if (isset($aValidTokens[$aToken['word_token']]))
544 $aValidTokens[$aToken['word_token']][] = $aToken;
548 $aValidTokens[$aToken['word_token']] = array($aToken);
550 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
551 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
553 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
555 // Try and calculate GB postcodes we might be missing
556 foreach($aTokens as $sToken)
558 // Source of gb postcodes is now definitive - always use
559 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
561 if (substr($aData[1],-2,1) != ' ')
563 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
564 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
566 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
567 if ($aGBPostcodeLocation)
569 $aValidTokens[$sToken] = $aGBPostcodeLocation;
572 // US ZIP+4 codes - if there is no token,
573 // merge in the 5-digit ZIP code
574 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
576 if (isset($aValidTokens[$aData[1]]))
578 foreach($aValidTokens[$aData[1]] as $aToken)
580 if (!$aToken['class'])
582 if (isset($aValidTokens[$sToken]))
584 $aValidTokens[$sToken][] = $aToken;
588 $aValidTokens[$sToken] = array($aToken);
596 foreach($aTokens as $sToken)
598 // Unknown single word token with a number - assume it is a house number
599 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
601 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
605 // Any words that have failed completely?
608 // Start the search process
609 $aResultPlaceIDs = array();
612 Calculate all searches using aValidTokens i.e.
613 'Wodsworth Road, Sheffield' =>
617 0 1 (wodsworth)(road)
620 Score how good the search is so they can be ordered
622 foreach($aPhrases as $iPhrase => $sPhrase)
624 $aNewPhraseSearches = array();
625 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
626 else $sPhraseType = '';
628 foreach($aPhrases[$iPhrase]['wordsets'] as $aWordset)
630 $aWordsetSearches = $aSearches;
632 // Add all words from this wordset
633 foreach($aWordset as $iToken => $sToken)
635 //echo "<br><b>$sToken</b>";
636 $aNewWordsetSearches = array();
638 foreach($aWordsetSearches as $aCurrentSearch)
641 //var_dump($aCurrentSearch);
644 // If the token is valid
645 if (isset($aValidTokens[' '.$sToken]))
647 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
649 $aSearch = $aCurrentSearch;
650 $aSearch['iSearchRank']++;
651 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
653 if ($aSearch['sCountryCode'] === false)
655 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
656 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
657 // If reverse order is enabled, it may appear at the beginning as well.
658 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
659 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
661 $aSearch['iSearchRank'] += 5;
663 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
666 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
668 if ($aSearch['fLat'] === '')
670 $aSearch['fLat'] = $aSearchTerm['lat'];
671 $aSearch['fLon'] = $aSearchTerm['lon'];
672 $aSearch['fRadius'] = $aSearchTerm['radius'];
673 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
676 elseif ($sPhraseType == 'postalcode')
678 // 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
679 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
681 // If we already have a name try putting the postcode first
682 if (sizeof($aSearch['aName']))
684 $aNewSearch = $aSearch;
685 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
686 $aNewSearch['aName'] = array();
687 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
688 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
691 if (sizeof($aSearch['aName']))
693 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
695 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
699 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
700 $aSearch['iSearchRank'] += 1000; // skip;
705 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
706 //$aSearch['iNamePhrase'] = $iPhrase;
708 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
712 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
714 if ($aSearch['sHouseNumber'] === '')
716 $aSearch['sHouseNumber'] = $sToken;
717 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
719 // Fall back to not searching for this item (better than nothing)
720 $aSearch = $aCurrentSearch;
721 $aSearch['iSearchRank'] += 1;
722 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
726 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
728 if ($aSearch['sClass'] === '')
730 $aSearch['sOperator'] = $aSearchTerm['operator'];
731 $aSearch['sClass'] = $aSearchTerm['class'];
732 $aSearch['sType'] = $aSearchTerm['type'];
733 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
734 else $aSearch['sOperator'] = 'near'; // near = in for the moment
735 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
737 // Do we have a shortcut id?
738 if ($aSearch['sOperator'] == 'name')
740 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
741 if ($iAmenityID = $this->oDB->getOne($sSQL))
743 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
744 $aSearch['aName'][$iAmenityID] = $iAmenityID;
745 $aSearch['sClass'] = '';
746 $aSearch['sType'] = '';
749 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
752 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
754 if (sizeof($aSearch['aName']))
756 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
758 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
762 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
763 $aSearch['iSearchRank'] += 1000; // skip;
768 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
769 //$aSearch['iNamePhrase'] = $iPhrase;
771 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
775 if (isset($aValidTokens[$sToken]))
777 // Allow searching for a word - but at extra cost
778 foreach($aValidTokens[$sToken] as $aSearchTerm)
780 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
782 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
784 $aSearch = $aCurrentSearch;
785 $aSearch['iSearchRank'] += 1;
786 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
788 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
789 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
791 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
793 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
795 if (empty($aSearchTermToken['country_code'])
796 && empty($aSearchTermToken['lat'])
797 && empty($aSearchTermToken['class']))
799 $aSearch = $aCurrentSearch;
800 $aSearch['iSearchRank'] += 1;
801 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
802 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
808 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
809 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
813 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
815 $aSearch = $aCurrentSearch;
816 $aSearch['iSearchRank'] += 2;
817 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
818 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
819 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
821 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
822 $aSearch['iNamePhrase'] = $iPhrase;
823 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
830 // Allow skipping a word - but at EXTREAM cost
831 //$aSearch = $aCurrentSearch;
832 //$aSearch['iSearchRank']+=100;
833 //$aNewWordsetSearches[] = $aSearch;
837 usort($aNewWordsetSearches, 'bySearchRank');
838 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
840 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
842 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
843 usort($aNewPhraseSearches, 'bySearchRank');
845 $aSearchHash = array();
846 foreach($aNewPhraseSearches as $iSearch => $aSearch)
848 $sHash = serialize($aSearch);
849 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
850 else $aSearchHash[$sHash] = 1;
853 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
856 // Re-group the searches by their score, junk anything over 20 as just not worth trying
857 $aGroupedSearches = array();
858 foreach($aNewPhraseSearches as $aSearch)
860 if ($aSearch['iSearchRank'] < $this->iMaxRank)
862 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
863 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
866 ksort($aGroupedSearches);
869 $aSearches = array();
870 foreach($aGroupedSearches as $iScore => $aNewSearches)
872 $iSearchCount += sizeof($aNewSearches);
873 $aSearches = array_merge($aSearches, $aNewSearches);
874 if ($iSearchCount > 50) break;
877 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
884 // Re-group the searches by their score, junk anything over 20 as just not worth trying
885 $aGroupedSearches = array();
886 foreach($aSearches as $aSearch)
888 if ($aSearch['iSearchRank'] < $this->iMaxRank)
890 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
891 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
894 ksort($aGroupedSearches);
897 if (CONST_Debug) var_Dump($aGroupedSearches);
899 if ($this->bReverseInPlan)
901 $aCopyGroupedSearches = $aGroupedSearches;
902 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
904 foreach($aSearches as $iSearch => $aSearch)
906 if (sizeof($aSearch['aAddress']))
908 $iReverseItem = array_pop($aSearch['aAddress']);
909 if (isset($aPossibleMainWordIDs[$iReverseItem]))
911 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
912 $aSearch['aName'] = array($iReverseItem);
913 $aGroupedSearches[$iGroup][] = $aSearch;
915 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
916 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
922 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
924 $aCopyGroupedSearches = $aGroupedSearches;
925 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
927 foreach($aSearches as $iSearch => $aSearch)
929 $aReductionsList = array($aSearch['aAddress']);
930 $iSearchRank = $aSearch['iSearchRank'];
931 while(sizeof($aReductionsList) > 0)
934 if ($iSearchRank > iMaxRank) break 3;
935 $aNewReductionsList = array();
936 foreach($aReductionsList as $aReductionsWordList)
938 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
940 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
941 $aReverseSearch = $aSearch;
942 $aSearch['aAddress'] = $aReductionsWordListResult;
943 $aSearch['iSearchRank'] = $iSearchRank;
944 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
945 if (sizeof($aReductionsWordListResult) > 0)
947 $aNewReductionsList[] = $aReductionsWordListResult;
951 $aReductionsList = $aNewReductionsList;
955 ksort($aGroupedSearches);
958 // Filter out duplicate searches
959 $aSearchHash = array();
960 foreach($aGroupedSearches as $iGroup => $aSearches)
962 foreach($aSearches as $iSearch => $aSearch)
964 $sHash = serialize($aSearch);
965 if (isset($aSearchHash[$sHash]))
967 unset($aGroupedSearches[$iGroup][$iSearch]);
968 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
972 $aSearchHash[$sHash] = 1;
977 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
981 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
984 foreach($aSearches as $aSearch)
988 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
989 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
992 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
994 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
996 // Just looking for a country by code - look it up
997 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
999 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1000 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1001 $sSQL .= " order by st_area(geometry) desc limit 1";
1002 if (CONST_Debug) var_dump($sSQL);
1003 $aPlaceIDs = $this->oDB->getCol($sSQL);
1008 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1009 if (!$aSearch['sClass']) continue;
1010 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1011 if ($this->oDB->getOne($sSQL))
1013 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1014 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1015 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1016 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1017 if (sizeof($this->aExcludePlaceIDs))
1019 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1021 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1022 $sSQL .= " limit $this->iLimit";
1023 if (CONST_Debug) var_dump($sSQL);
1024 $aPlaceIDs = $this->oDB->getCol($sSQL);
1026 // If excluded place IDs are given, it is fair to assume that
1027 // there have been results in the small box, so no further
1028 // expansion in that case.
1029 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1031 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1032 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1033 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1034 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1035 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1036 $sSQL .= " limit $this->iLimit";
1037 if (CONST_Debug) var_dump($sSQL);
1038 $aPlaceIDs = $this->oDB->getCol($sSQL);
1043 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1044 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1045 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1046 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1047 $sSQL .= " limit $this->iLimit";
1048 if (CONST_Debug) var_dump($sSQL);
1049 $aPlaceIDs = $this->oDB->getCol($sSQL);
1055 $aPlaceIDs = array();
1057 // First we need a position, either aName or fLat or both
1061 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1062 // they might be right - but they are just too darned expensive to run
1063 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1064 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1065 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1067 // For infrequent name terms disable index usage for address
1068 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1069 sizeof($aSearch['aName']) == 1 &&
1070 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1072 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1076 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1077 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1080 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1081 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1082 if ($aSearch['fLon'] && $aSearch['fLat'])
1084 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1085 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1087 if (sizeof($this->aExcludePlaceIDs))
1089 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1091 if ($sCountryCodesSQL)
1093 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1096 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1097 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1099 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1100 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1101 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1102 $aOrder[] = "$sImportanceSQL DESC";
1103 if (sizeof($aSearch['aFullNameAddress']))
1105 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1108 if (sizeof($aTerms))
1110 $sSQL = "select place_id";
1111 $sSQL .= " from search_name";
1112 $sSQL .= " where ".join(' and ',$aTerms);
1113 $sSQL .= " order by ".join(', ',$aOrder);
1114 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1115 $sSQL .= " limit 50";
1116 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1117 $sSQL .= " limit 1";
1119 $sSQL .= " limit ".$this->iLimit;
1121 if (CONST_Debug) { var_dump($sSQL); }
1122 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1123 if (PEAR::IsError($aViewBoxPlaceIDs))
1125 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1127 //var_dump($aViewBoxPlaceIDs);
1128 // Did we have an viewbox matches?
1129 $aPlaceIDs = array();
1130 $bViewBoxMatch = false;
1131 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1133 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1134 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1135 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1136 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1137 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1140 //var_Dump($aPlaceIDs);
1143 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1145 $aRoadPlaceIDs = $aPlaceIDs;
1146 $sPlaceIDs = join(',',$aPlaceIDs);
1148 // Now they are indexed look for a house attached to a street we found
1149 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1150 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1151 if (sizeof($this->aExcludePlaceIDs))
1153 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1155 $sSQL .= " limit $this->iLimit";
1156 if (CONST_Debug) var_dump($sSQL);
1157 $aPlaceIDs = $this->oDB->getCol($sSQL);
1159 // If not try the aux fallback table
1161 if (!sizeof($aPlaceIDs))
1163 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1164 if (sizeof($this->aExcludePlaceIDs))
1166 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1168 //$sSQL .= " limit $this->iLimit";
1169 if (CONST_Debug) var_dump($sSQL);
1170 $aPlaceIDs = $this->oDB->getCol($sSQL);
1174 if (!sizeof($aPlaceIDs))
1176 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1177 if (sizeof($this->aExcludePlaceIDs))
1179 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1181 //$sSQL .= " limit $this->iLimit";
1182 if (CONST_Debug) var_dump($sSQL);
1183 $aPlaceIDs = $this->oDB->getCol($sSQL);
1186 // Fallback to the road
1187 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1189 $aPlaceIDs = $aRoadPlaceIDs;
1194 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1196 $sPlaceIDs = join(',',$aPlaceIDs);
1197 $aClassPlaceIDs = array();
1199 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1201 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1202 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1203 $sSQL .= " and linked_place_id is null";
1204 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1205 $sSQL .= " order by rank_search asc limit $this->iLimit";
1206 if (CONST_Debug) var_dump($sSQL);
1207 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1210 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1212 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1213 $bCacheTable = $this->oDB->getOne($sSQL);
1215 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1217 if (CONST_Debug) var_dump($sSQL);
1218 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1220 // For state / country level searches the normal radius search doesn't work very well
1221 $sPlaceGeom = false;
1222 if ($this->iMaxRank < 9 && $bCacheTable)
1224 // Try and get a polygon to search in instead
1225 $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";
1226 if (CONST_Debug) var_dump($sSQL);
1227 $sPlaceGeom = $this->oDB->getOne($sSQL);
1236 $this->iMaxRank += 5;
1237 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1238 if (CONST_Debug) var_dump($sSQL);
1239 $aPlaceIDs = $this->oDB->getCol($sSQL);
1240 $sPlaceIDs = join(',',$aPlaceIDs);
1243 if ($sPlaceIDs || $sPlaceGeom)
1249 // More efficient - can make the range bigger
1253 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1254 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1255 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1257 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1258 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1261 $sSQL .= ",placex as f where ";
1262 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1267 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1269 if (sizeof($this->aExcludePlaceIDs))
1271 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1273 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1274 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1275 if ($iOffset) $sSQL .= " offset $iOffset";
1276 $sSQL .= " limit $this->iLimit";
1277 if (CONST_Debug) var_dump($sSQL);
1278 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1282 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1285 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1286 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1288 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1289 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1290 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1291 if (sizeof($this->aExcludePlaceIDs))
1293 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1295 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1296 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1297 if ($iOffset) $sSQL .= " offset $iOffset";
1298 $sSQL .= " limit $this->iLimit";
1299 if (CONST_Debug) var_dump($sSQL);
1300 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1305 $aPlaceIDs = $aClassPlaceIDs;
1311 if (PEAR::IsError($aPlaceIDs))
1313 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1316 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1318 foreach($aPlaceIDs as $iPlaceID)
1320 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1322 if ($iQueryLoop > 20) break;
1325 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1327 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1328 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1329 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1330 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1331 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1332 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1333 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1334 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1336 if (CONST_Debug) var_dump($sSQL);
1337 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1341 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1342 if ($iGroupLoop > 4) break;
1343 if ($iQueryLoop > 30) break;
1346 // Did we find anything?
1347 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1349 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1355 // Just interpret as a reverse geocode
1356 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1358 $aSearchResults = $this->getDetails(array($iPlaceID));
1360 $aSearchResults = array();
1364 if (!sizeof($aSearchResults))
1369 $aClassType = getClassTypesWithImportance();
1370 $aRecheckWords = preg_split('/\b/u',$sQuery);
1371 foreach($aRecheckWords as $i => $sWord)
1373 if (!$sWord) unset($aRecheckWords[$i]);
1376 foreach($aSearchResults as $iResNum => $aResult)
1378 if (CONST_Search_AreaPolygons)
1380 // Get the bounding box and outline polygon
1381 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1382 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1383 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1384 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1385 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1386 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1387 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1388 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1389 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1390 $aPointPolygon = $this->oDB->getRow($sSQL);
1391 if (PEAR::IsError($aPointPolygon))
1393 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1396 if ($aPointPolygon['place_id'])
1398 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1399 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1400 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1401 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1403 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1405 $aResult['lat'] = $aPointPolygon['centrelat'];
1406 $aResult['lon'] = $aPointPolygon['centrelon'];
1409 if ($this->bIncludePolygonAsPoints)
1411 // Translate geometary string to point array
1412 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1414 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1416 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1418 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1420 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1423 $iSteps = ($fRadius * 40000)^2;
1424 $fStepSize = (2*pi())/$iSteps;
1425 $aPolyPoints = array();
1426 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1428 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1430 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1431 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1432 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1433 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1437 // Output data suitable for display (points and a bounding box)
1438 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1440 $aResult['aPolyPoints'] = array();
1441 foreach($aPolyPoints as $aPoint)
1443 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1446 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1450 if ($aResult['extra_place'] == 'city')
1452 $aResult['class'] = 'place';
1453 $aResult['type'] = 'city';
1454 $aResult['rank_search'] = 16;
1457 if (!isset($aResult['aBoundingBox']))
1460 $fDiameter = 0.0001;
1462 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1463 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1465 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1467 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1468 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1470 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1472 $fRadius = $fDiameter / 2;
1474 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1475 $fStepSize = (2*pi())/$iSteps;
1476 $aPolyPoints = array();
1477 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1479 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1481 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1482 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1483 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1484 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1486 // Output data suitable for display (points and a bounding box)
1487 if ($this->bIncludePolygonAsPoints)
1489 $aResult['aPolyPoints'] = array();
1490 foreach($aPolyPoints as $aPoint)
1492 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1495 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1498 // Is there an icon set for this type of result?
1499 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1500 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1502 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1505 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1506 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1508 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1510 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1511 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1513 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1516 if ($this->bIncludeAddressDetails)
1518 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1519 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1521 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1525 // Adjust importance for the number of exact string matches in the result
1526 $aResult['importance'] = max(0.001,$aResult['importance']);
1528 $sAddress = $aResult['langaddress'];
1529 foreach($aRecheckWords as $i => $sWord)
1531 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1534 $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
1536 $aResult['name'] = $aResult['langaddress'];
1537 $aResult['foundorder'] = -$aResult['addressimportance'];
1538 $aSearchResults[$iResNum] = $aResult;
1540 uasort($aSearchResults, 'byImportance');
1542 $aOSMIDDone = array();
1543 $aClassTypeNameDone = array();
1544 $aToFilter = $aSearchResults;
1545 $aSearchResults = array();
1548 foreach($aToFilter as $iResNum => $aResult)
1550 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1551 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1554 $fLat = $aResult['lat'];
1555 $fLon = $aResult['lon'];
1556 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1559 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1560 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1562 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1563 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1564 $aSearchResults[] = $aResult;
1567 // Absolute limit on number of results
1568 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1571 return $aSearchResults;
1580 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1582 $aPoints = explode(',',$_GET['route']);
1583 if (sizeof($aPoints) % 2 != 0)
1585 userError("Uneven number of points");
1588 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1589 $fPrevCoord = false;