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 setReverseInPlan($bReverse)
50 $this->bReverseInPlan = $bReverse;
53 function setLanguagePreference($aLangPref)
55 $this->aLangPrefOrder = $aLangPref;
58 function setIncludeAddressDetails($bAddressDetails = true)
60 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
63 function getIncludeAddressDetails()
65 return $this->bIncludeAddressDetails;
68 function setIncludePolygonAsPoints($b = true)
70 $this->bIncludePolygonAsPoints = $b;
73 function getIncludePolygonAsPoints()
75 return $this->bIncludePolygonAsPoints;
78 function setIncludePolygonAsText($b = true)
80 $this->bIncludePolygonAsText = $b;
83 function getIncludePolygonAsText()
85 return $this->bIncludePolygonAsText;
88 function setIncludePolygonAsGeoJSON($b = true)
90 $this->bIncludePolygonAsGeoJSON = $b;
93 function setIncludePolygonAsKML($b = true)
95 $this->bIncludePolygonAsKML = $b;
98 function setIncludePolygonAsSVG($b = true)
100 $this->bIncludePolygonAsSVG = $b;
103 function setDeDupe($bDeDupe = true)
105 $this->bDeDupe = (bool)$bDeDupe;
108 function setLimit($iLimit = 10)
110 if ($iLimit > 50) $iLimit = 50;
111 if ($iLimit < 1) $iLimit = 1;
113 $this->iFinalLimit = $iLimit;
114 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
117 function setOffset($iOffset = 0)
119 $this->iOffset = $iOffset;
122 function setExcludedPlaceIDs($a)
124 // TODO: force to int
125 $this->aExcludePlaceIDs = $a;
128 function getExcludedPlaceIDs()
130 return $this->aExcludePlaceIDs;
133 function setBounded($bBoundedSearch = true)
135 $this->bBoundedSearch = (bool)$bBoundedSearch;
138 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
140 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
143 function getViewBoxString()
145 if (!$this->aViewBox) return null;
146 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
149 function setRoute($aRoutePoints)
151 $this->aRoutePoints = $aRoutePoints;
154 function setFeatureType($sFeatureType)
156 switch($sFeatureType)
159 $this->setRankRange(4, 4);
162 $this->setRankRange(8, 8);
165 $this->setRankRange(14, 16);
168 $this->setRankRange(8, 20);
173 function setRankRange($iMin, $iMax)
175 $this->iMinAddressRank = (int)$iMin;
176 $this->iMaxAddressRank = (int)$iMax;
179 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
181 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
184 function setCountryCodesList($aCountryCodes)
186 $this->aCountryCodes = $aCountryCodes;
189 function setQuery($sQueryString)
191 $this->sQuery = $sQueryString;
192 $this->aStructuredQuery = false;
195 function getQueryString()
197 return $this->sQuery;
200 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
202 $sValue = trim($sValue);
203 if (!$sValue) return false;
204 $this->aStructuredQuery[$sKey] = $sValue;
205 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
207 $this->iMinAddressRank = $iNewMinAddressRank;
208 $this->iMaxAddressRank = $iNewMaxAddressRank;
210 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
214 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
216 $this->sQuery = false;
218 $this->aStructuredQuery = array();
219 $this->sAllowedTypesSQLList = '';
221 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
222 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
223 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
224 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
225 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
226 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
227 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
229 if (sizeof($this->aStructuredQuery) > 0)
231 $this->sQuery = join(', ', $this->aStructuredQuery);
232 if ($this->iMaxAddressRank < 30)
234 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
240 function getDetails($aPlaceIDs)
242 if (sizeof($aPlaceIDs) == 0) return array();
244 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
246 // Get the details for display (is this a redundant extra step?)
247 $sPlaceIDs = join(',',$aPlaceIDs);
249 $sImportanceSQL = '';
250 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
251 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
253 $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,";
254 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
255 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
256 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
257 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
258 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
259 $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, ";
260 $sSQL .= "(extratags->'place') as extra_place ";
261 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
262 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
263 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
264 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
266 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
267 $sSQL .= "and linked_place_id is null ";
268 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
269 if (!$this->bDeDupe) $sSQL .= ",place_id";
270 $sSQL .= ",langaddress ";
271 $sSQL .= ",placename ";
273 $sSQL .= ",extratags->'place' ";
275 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
278 $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,";
279 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
280 $sSQL .= "null as placename,";
281 $sSQL .= "null as ref,";
282 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
283 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
284 $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, ";
285 $sSQL .= "null as extra_place ";
286 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
287 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
288 $sSQL .= "group by place_id";
289 if (!$this->bDeDupe) $sSQL .= ",place_id ";
292 $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,";
293 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
294 $sSQL .= "null as placename,";
295 $sSQL .= "null as ref,";
296 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
297 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
298 $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, ";
299 $sSQL .= "null as extra_place ";
300 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
301 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
302 $sSQL .= "group by place_id";
303 if (!$this->bDeDupe) $sSQL .= ",place_id";
304 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
308 $sSQL .= " order by importance desc";
309 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
310 $aSearchResults = $this->oDB->getAll($sSQL);
312 if (PEAR::IsError($aSearchResults))
314 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
317 return $aSearchResults;
322 if (!$this->sQuery && !$this->aStructuredQuery) return false;
324 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
326 $sCountryCodesSQL = false;
327 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
329 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
332 // Hack to make it handle "new york, ny" (and variants) correctly
333 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
335 // Conflicts between US state abreviations and various words for 'the' in different languages
336 if (isset($this->aLangPrefOrder['name:en']))
338 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
339 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
340 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
345 $bBoundingBoxSearch = false;
348 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
349 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
350 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
351 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
352 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
353 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
355 $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)";
356 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
357 $bBoundingBoxSearch = $this->bBoundedSearch;
361 if ($this->aRoutePoints)
363 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
365 foreach($this->aRouteaPoints as $aPoint)
367 if (!$bFirst) $sViewboxCentreSQL .= ",";
368 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
370 $sViewboxCentreSQL .= ")'::geometry,4326)";
372 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
373 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
374 if (PEAR::isError($this->sViewboxSmallSQL))
376 failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
378 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
380 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
381 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
382 if (PEAR::isError($this->sViewboxLargeSQL))
384 failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
386 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
387 $bBoundingBoxSearch = $this->bBoundedSearch;
390 // Do we have anything that looks like a lat/lon pair?
391 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
393 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
394 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
395 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
397 $this->setNearPoint(array($fQueryLat, $fQueryLon));
398 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
401 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
403 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
404 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
405 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
407 $this->setNearPoint(array($fQueryLat, $fQueryLon));
408 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
411 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
413 $fQueryLat = $aData[2];
414 $fQueryLon = $aData[3];
415 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
417 $this->setNearPoint(array($fQueryLat, $fQueryLon));
418 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
422 $aSearchResults = array();
423 if ($sQuery || $this->aStructuredQuery)
425 // Start with a blank search
427 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
428 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
429 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
432 // Do we have a radius search?
433 $sNearPointSQL = false;
434 if ($this->aNearPoint)
436 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
437 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
438 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
439 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
442 // Any 'special' terms in the search?
443 $bSpecialTerms = false;
444 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
445 $aSpecialTerms = array();
446 foreach($aSpecialTermsRaw as $aSpecialTerm)
448 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
449 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
452 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
453 $aSpecialTerms = array();
454 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
456 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
457 unset($aStructuredQuery['amenity']);
459 foreach($aSpecialTermsRaw as $aSpecialTerm)
461 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
462 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
463 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
464 $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';
465 if (CONST_Debug) var_Dump($sSQL);
466 $aSearchWords = $this->oDB->getAll($sSQL);
467 $aNewSearches = array();
468 foreach($aSearches as $aSearch)
470 foreach($aSearchWords as $aSearchTerm)
472 $aNewSearch = $aSearch;
473 if ($aSearchTerm['country_code'])
475 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
476 $aNewSearches[] = $aNewSearch;
477 $bSpecialTerms = true;
479 if ($aSearchTerm['class'])
481 $aNewSearch['sClass'] = $aSearchTerm['class'];
482 $aNewSearch['sType'] = $aSearchTerm['type'];
483 $aNewSearches[] = $aNewSearch;
484 $bSpecialTerms = true;
488 $aSearches = $aNewSearches;
491 // Split query into phrases
492 // Commas are used to reduce the search space by indicating where phrases split
493 if ($this->aStructuredQuery)
495 $aPhrases = $this->aStructuredQuery;
496 $bStructuredPhrases = true;
500 $aPhrases = explode(',',$sQuery);
501 $bStructuredPhrases = false;
504 // Convert each phrase to standard form
505 // Create a list of standard words
506 // Get all 'sets' of words
507 // Generate a complete list of all
509 foreach($aPhrases as $iPhrase => $sPhrase)
511 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
512 if (PEAR::isError($aPhrase))
514 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
515 if (CONST_Debug) var_dump($aPhrase);
518 if (trim($aPhrase['string']))
520 $aPhrases[$iPhrase] = $aPhrase;
521 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
522 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
523 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
527 unset($aPhrases[$iPhrase]);
531 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
532 $aPhraseTypes = array_keys($aPhrases);
533 $aPhrases = array_values($aPhrases);
535 if (sizeof($aTokens))
537 // Check which tokens we have, get the ID numbers
538 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
539 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
541 if (CONST_Debug) var_Dump($sSQL);
543 $aValidTokens = array();
544 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
545 else $aDatabaseWords = array();
546 if (PEAR::IsError($aDatabaseWords))
548 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
550 $aPossibleMainWordIDs = array();
551 $aWordFrequencyScores = array();
552 foreach($aDatabaseWords as $aToken)
554 // Very special case - require 2 letter country param to match the country code found
555 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
556 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
561 if (isset($aValidTokens[$aToken['word_token']]))
563 $aValidTokens[$aToken['word_token']][] = $aToken;
567 $aValidTokens[$aToken['word_token']] = array($aToken);
569 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
570 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
572 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
574 // Try and calculate GB postcodes we might be missing
575 foreach($aTokens as $sToken)
577 // Source of gb postcodes is now definitive - always use
578 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
580 if (substr($aData[1],-2,1) != ' ')
582 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
583 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
585 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
586 if ($aGBPostcodeLocation)
588 $aValidTokens[$sToken] = $aGBPostcodeLocation;
591 // US ZIP+4 codes - if there is no token,
592 // merge in the 5-digit ZIP code
593 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
595 if (isset($aValidTokens[$aData[1]]))
597 foreach($aValidTokens[$aData[1]] as $aToken)
599 if (!$aToken['class'])
601 if (isset($aValidTokens[$sToken]))
603 $aValidTokens[$sToken][] = $aToken;
607 $aValidTokens[$sToken] = array($aToken);
615 foreach($aTokens as $sToken)
617 // Unknown single word token with a number - assume it is a house number
618 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
620 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
624 // Any words that have failed completely?
627 // Start the search process
628 $aResultPlaceIDs = array();
631 Calculate all searches using aValidTokens i.e.
632 'Wodsworth Road, Sheffield' =>
636 0 1 (wodsworth)(road)
639 Score how good the search is so they can be ordered
641 foreach($aPhrases as $iPhrase => $sPhrase)
643 $aNewPhraseSearches = array();
644 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
645 else $sPhraseType = '';
647 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
649 // Too many permutations - too expensive
650 if ($iWordSet > 120) break;
652 $aWordsetSearches = $aSearches;
654 // Add all words from this wordset
655 foreach($aWordset as $iToken => $sToken)
657 //echo "<br><b>$sToken</b>";
658 $aNewWordsetSearches = array();
660 foreach($aWordsetSearches as $aCurrentSearch)
663 //var_dump($aCurrentSearch);
666 // If the token is valid
667 if (isset($aValidTokens[' '.$sToken]))
669 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
671 $aSearch = $aCurrentSearch;
672 $aSearch['iSearchRank']++;
673 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
675 if ($aSearch['sCountryCode'] === false)
677 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
678 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
679 // If reverse order is enabled, it may appear at the beginning as well.
680 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
681 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
683 $aSearch['iSearchRank'] += 5;
685 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
688 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
690 if ($aSearch['fLat'] === '')
692 $aSearch['fLat'] = $aSearchTerm['lat'];
693 $aSearch['fLon'] = $aSearchTerm['lon'];
694 $aSearch['fRadius'] = $aSearchTerm['radius'];
695 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
698 elseif ($sPhraseType == 'postalcode')
700 // 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
701 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
703 // If we already have a name try putting the postcode first
704 if (sizeof($aSearch['aName']))
706 $aNewSearch = $aSearch;
707 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
708 $aNewSearch['aName'] = array();
709 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
710 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
713 if (sizeof($aSearch['aName']))
715 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
717 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
721 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
722 $aSearch['iSearchRank'] += 1000; // skip;
727 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
728 //$aSearch['iNamePhrase'] = $iPhrase;
730 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
734 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
736 if ($aSearch['sHouseNumber'] === '')
738 $aSearch['sHouseNumber'] = $sToken;
739 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
741 // Fall back to not searching for this item (better than nothing)
742 $aSearch = $aCurrentSearch;
743 $aSearch['iSearchRank'] += 1;
744 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
748 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
750 if ($aSearch['sClass'] === '')
752 $aSearch['sOperator'] = $aSearchTerm['operator'];
753 $aSearch['sClass'] = $aSearchTerm['class'];
754 $aSearch['sType'] = $aSearchTerm['type'];
755 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
756 else $aSearch['sOperator'] = 'near'; // near = in for the moment
757 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
759 // Do we have a shortcut id?
760 if ($aSearch['sOperator'] == 'name')
762 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
763 if ($iAmenityID = $this->oDB->getOne($sSQL))
765 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
766 $aSearch['aName'][$iAmenityID] = $iAmenityID;
767 $aSearch['sClass'] = '';
768 $aSearch['sType'] = '';
771 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
774 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
776 if (sizeof($aSearch['aName']))
778 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
780 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
784 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
785 $aSearch['iSearchRank'] += 1000; // skip;
790 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
791 //$aSearch['iNamePhrase'] = $iPhrase;
793 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
797 if (isset($aValidTokens[$sToken]))
799 // Allow searching for a word - but at extra cost
800 foreach($aValidTokens[$sToken] as $aSearchTerm)
802 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
804 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
806 $aSearch = $aCurrentSearch;
807 $aSearch['iSearchRank'] += 1;
808 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
810 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
811 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
813 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
815 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
817 if (empty($aSearchTermToken['country_code'])
818 && empty($aSearchTermToken['lat'])
819 && empty($aSearchTermToken['class']))
821 $aSearch = $aCurrentSearch;
822 $aSearch['iSearchRank'] += 1;
823 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
824 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
830 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
831 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
835 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
837 $aSearch = $aCurrentSearch;
838 $aSearch['iSearchRank'] += 2;
839 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
840 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
841 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
843 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
844 $aSearch['iNamePhrase'] = $iPhrase;
845 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
852 // Allow skipping a word - but at EXTREAM cost
853 //$aSearch = $aCurrentSearch;
854 //$aSearch['iSearchRank']+=100;
855 //$aNewWordsetSearches[] = $aSearch;
859 usort($aNewWordsetSearches, 'bySearchRank');
860 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
862 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
864 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
865 usort($aNewPhraseSearches, 'bySearchRank');
867 $aSearchHash = array();
868 foreach($aNewPhraseSearches as $iSearch => $aSearch)
870 $sHash = serialize($aSearch);
871 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
872 else $aSearchHash[$sHash] = 1;
875 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
878 // Re-group the searches by their score, junk anything over 20 as just not worth trying
879 $aGroupedSearches = array();
880 foreach($aNewPhraseSearches as $aSearch)
882 if ($aSearch['iSearchRank'] < $this->iMaxRank)
884 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
885 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
888 ksort($aGroupedSearches);
891 $aSearches = array();
892 foreach($aGroupedSearches as $iScore => $aNewSearches)
894 $iSearchCount += sizeof($aNewSearches);
895 $aSearches = array_merge($aSearches, $aNewSearches);
896 if ($iSearchCount > 50) break;
899 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
906 // Re-group the searches by their score, junk anything over 20 as just not worth trying
907 $aGroupedSearches = array();
908 foreach($aSearches as $aSearch)
910 if ($aSearch['iSearchRank'] < $this->iMaxRank)
912 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
913 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
916 ksort($aGroupedSearches);
919 if (CONST_Debug) var_Dump($aGroupedSearches);
921 if ($this->bReverseInPlan)
923 $aCopyGroupedSearches = $aGroupedSearches;
924 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
926 foreach($aSearches as $iSearch => $aSearch)
928 if (sizeof($aSearch['aAddress']))
930 $iReverseItem = array_pop($aSearch['aAddress']);
931 if (isset($aPossibleMainWordIDs[$iReverseItem]))
933 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
934 $aSearch['aName'] = array($iReverseItem);
935 $aGroupedSearches[$iGroup][] = $aSearch;
937 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
938 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
944 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
946 $aCopyGroupedSearches = $aGroupedSearches;
947 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
949 foreach($aSearches as $iSearch => $aSearch)
951 $aReductionsList = array($aSearch['aAddress']);
952 $iSearchRank = $aSearch['iSearchRank'];
953 while(sizeof($aReductionsList) > 0)
956 if ($iSearchRank > iMaxRank) break 3;
957 $aNewReductionsList = array();
958 foreach($aReductionsList as $aReductionsWordList)
960 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
962 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
963 $aReverseSearch = $aSearch;
964 $aSearch['aAddress'] = $aReductionsWordListResult;
965 $aSearch['iSearchRank'] = $iSearchRank;
966 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
967 if (sizeof($aReductionsWordListResult) > 0)
969 $aNewReductionsList[] = $aReductionsWordListResult;
973 $aReductionsList = $aNewReductionsList;
977 ksort($aGroupedSearches);
980 // Filter out duplicate searches
981 $aSearchHash = array();
982 foreach($aGroupedSearches as $iGroup => $aSearches)
984 foreach($aSearches as $iSearch => $aSearch)
986 $sHash = serialize($aSearch);
987 if (isset($aSearchHash[$sHash]))
989 unset($aGroupedSearches[$iGroup][$iSearch]);
990 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
994 $aSearchHash[$sHash] = 1;
999 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1003 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1006 foreach($aSearches as $aSearch)
1010 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1011 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1013 // No location term?
1014 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1016 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1018 // Just looking for a country by code - look it up
1019 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1021 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1022 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1023 $sSQL .= " order by st_area(geometry) desc limit 1";
1024 if (CONST_Debug) var_dump($sSQL);
1025 $aPlaceIDs = $this->oDB->getCol($sSQL);
1030 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1031 if (!$aSearch['sClass']) continue;
1032 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1033 if ($this->oDB->getOne($sSQL))
1035 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1036 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1037 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1038 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1039 if (sizeof($this->aExcludePlaceIDs))
1041 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1043 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1044 $sSQL .= " limit $this->iLimit";
1045 if (CONST_Debug) var_dump($sSQL);
1046 $aPlaceIDs = $this->oDB->getCol($sSQL);
1048 // If excluded place IDs are given, it is fair to assume that
1049 // there have been results in the small box, so no further
1050 // expansion in that case.
1051 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1053 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1054 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1055 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1056 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1057 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1058 $sSQL .= " limit $this->iLimit";
1059 if (CONST_Debug) var_dump($sSQL);
1060 $aPlaceIDs = $this->oDB->getCol($sSQL);
1065 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1066 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1067 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1068 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1069 $sSQL .= " limit $this->iLimit";
1070 if (CONST_Debug) var_dump($sSQL);
1071 $aPlaceIDs = $this->oDB->getCol($sSQL);
1077 $aPlaceIDs = array();
1079 // First we need a position, either aName or fLat or both
1083 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1084 // they might be right - but they are just too darned expensive to run
1085 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1086 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1087 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1089 // For infrequent name terms disable index usage for address
1090 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1091 sizeof($aSearch['aName']) == 1 &&
1092 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1094 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1098 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1099 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1102 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1103 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1104 if ($aSearch['fLon'] && $aSearch['fLat'])
1106 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1107 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1109 if (sizeof($this->aExcludePlaceIDs))
1111 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1113 if ($sCountryCodesSQL)
1115 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1118 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1119 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1121 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1122 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1123 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1124 $aOrder[] = "$sImportanceSQL DESC";
1125 if (sizeof($aSearch['aFullNameAddress']))
1127 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1130 if (sizeof($aTerms))
1132 $sSQL = "select place_id";
1133 $sSQL .= " from search_name";
1134 $sSQL .= " where ".join(' and ',$aTerms);
1135 $sSQL .= " order by ".join(', ',$aOrder);
1136 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1137 $sSQL .= " limit 50";
1138 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1139 $sSQL .= " limit 1";
1141 $sSQL .= " limit ".$this->iLimit;
1143 if (CONST_Debug) { var_dump($sSQL); }
1144 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1145 if (PEAR::IsError($aViewBoxPlaceIDs))
1147 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1149 //var_dump($aViewBoxPlaceIDs);
1150 // Did we have an viewbox matches?
1151 $aPlaceIDs = array();
1152 $bViewBoxMatch = false;
1153 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1155 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1156 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1157 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1158 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1159 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1162 //var_Dump($aPlaceIDs);
1165 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1167 $aRoadPlaceIDs = $aPlaceIDs;
1168 $sPlaceIDs = join(',',$aPlaceIDs);
1170 // Now they are indexed look for a house attached to a street we found
1171 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1172 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1173 if (sizeof($this->aExcludePlaceIDs))
1175 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1177 $sSQL .= " limit $this->iLimit";
1178 if (CONST_Debug) var_dump($sSQL);
1179 $aPlaceIDs = $this->oDB->getCol($sSQL);
1181 // If not try the aux fallback table
1183 if (!sizeof($aPlaceIDs))
1185 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1186 if (sizeof($this->aExcludePlaceIDs))
1188 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1190 //$sSQL .= " limit $this->iLimit";
1191 if (CONST_Debug) var_dump($sSQL);
1192 $aPlaceIDs = $this->oDB->getCol($sSQL);
1196 if (!sizeof($aPlaceIDs))
1198 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1199 if (sizeof($this->aExcludePlaceIDs))
1201 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1203 //$sSQL .= " limit $this->iLimit";
1204 if (CONST_Debug) var_dump($sSQL);
1205 $aPlaceIDs = $this->oDB->getCol($sSQL);
1208 // Fallback to the road
1209 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1211 $aPlaceIDs = $aRoadPlaceIDs;
1216 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1218 $sPlaceIDs = join(',',$aPlaceIDs);
1219 $aClassPlaceIDs = array();
1221 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1223 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1224 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1225 $sSQL .= " and linked_place_id is null";
1226 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1227 $sSQL .= " order by rank_search asc limit $this->iLimit";
1228 if (CONST_Debug) var_dump($sSQL);
1229 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1232 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1234 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1235 $bCacheTable = $this->oDB->getOne($sSQL);
1237 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1239 if (CONST_Debug) var_dump($sSQL);
1240 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1242 // For state / country level searches the normal radius search doesn't work very well
1243 $sPlaceGeom = false;
1244 if ($this->iMaxRank < 9 && $bCacheTable)
1246 // Try and get a polygon to search in instead
1247 $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";
1248 if (CONST_Debug) var_dump($sSQL);
1249 $sPlaceGeom = $this->oDB->getOne($sSQL);
1258 $this->iMaxRank += 5;
1259 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1260 if (CONST_Debug) var_dump($sSQL);
1261 $aPlaceIDs = $this->oDB->getCol($sSQL);
1262 $sPlaceIDs = join(',',$aPlaceIDs);
1265 if ($sPlaceIDs || $sPlaceGeom)
1271 // More efficient - can make the range bigger
1275 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1276 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1277 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1279 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1280 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1283 $sSQL .= ",placex as f where ";
1284 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1289 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1291 if (sizeof($this->aExcludePlaceIDs))
1293 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1295 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1296 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." 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));
1304 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1307 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1308 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1310 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1311 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1312 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1313 if (sizeof($this->aExcludePlaceIDs))
1315 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1317 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1318 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1319 if ($iOffset) $sSQL .= " offset $iOffset";
1320 $sSQL .= " limit $this->iLimit";
1321 if (CONST_Debug) var_dump($sSQL);
1322 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1327 $aPlaceIDs = $aClassPlaceIDs;
1333 if (PEAR::IsError($aPlaceIDs))
1335 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1338 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1340 foreach($aPlaceIDs as $iPlaceID)
1342 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1344 if ($iQueryLoop > 20) break;
1347 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1349 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1350 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1351 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1352 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1353 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1354 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1355 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1356 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1358 if (CONST_Debug) var_dump($sSQL);
1359 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1363 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1364 if ($iGroupLoop > 4) break;
1365 if ($iQueryLoop > 30) break;
1368 // Did we find anything?
1369 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1371 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1377 // Just interpret as a reverse geocode
1378 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1380 $aSearchResults = $this->getDetails(array($iPlaceID));
1382 $aSearchResults = array();
1386 if (!sizeof($aSearchResults))
1391 $aClassType = getClassTypesWithImportance();
1392 $aRecheckWords = preg_split('/\b/u',$sQuery);
1393 foreach($aRecheckWords as $i => $sWord)
1395 if (!$sWord) unset($aRecheckWords[$i]);
1398 foreach($aSearchResults as $iResNum => $aResult)
1400 if (CONST_Search_AreaPolygons)
1402 // Get the bounding box and outline polygon
1403 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1404 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1405 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1406 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1407 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1408 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1409 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1410 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1411 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1412 $aPointPolygon = $this->oDB->getRow($sSQL);
1413 if (PEAR::IsError($aPointPolygon))
1415 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1418 if ($aPointPolygon['place_id'])
1420 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1421 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1422 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1423 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1425 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1427 $aResult['lat'] = $aPointPolygon['centrelat'];
1428 $aResult['lon'] = $aPointPolygon['centrelon'];
1431 if ($this->bIncludePolygonAsPoints)
1433 // Translate geometary string to point array
1434 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1436 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1438 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1440 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1442 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1445 $iSteps = ($fRadius * 40000)^2;
1446 $fStepSize = (2*pi())/$iSteps;
1447 $aPolyPoints = array();
1448 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1450 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1452 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1453 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1454 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1455 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1459 // Output data suitable for display (points and a bounding box)
1460 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1462 $aResult['aPolyPoints'] = array();
1463 foreach($aPolyPoints as $aPoint)
1465 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1468 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1472 if ($aResult['extra_place'] == 'city')
1474 $aResult['class'] = 'place';
1475 $aResult['type'] = 'city';
1476 $aResult['rank_search'] = 16;
1479 if (!isset($aResult['aBoundingBox']))
1482 $fDiameter = 0.0001;
1484 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1485 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1487 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1489 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1490 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1492 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1494 $fRadius = $fDiameter / 2;
1496 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1497 $fStepSize = (2*pi())/$iSteps;
1498 $aPolyPoints = array();
1499 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1501 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1503 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1504 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1505 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1506 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1508 // Output data suitable for display (points and a bounding box)
1509 if ($this->bIncludePolygonAsPoints)
1511 $aResult['aPolyPoints'] = array();
1512 foreach($aPolyPoints as $aPoint)
1514 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1517 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1520 // Is there an icon set for this type of result?
1521 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1522 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1524 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1527 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1528 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1530 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1532 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1533 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1535 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1538 if ($this->bIncludeAddressDetails)
1540 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1541 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1543 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1547 // Adjust importance for the number of exact string matches in the result
1548 $aResult['importance'] = max(0.001,$aResult['importance']);
1550 $sAddress = $aResult['langaddress'];
1551 foreach($aRecheckWords as $i => $sWord)
1553 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1556 $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
1558 $aResult['name'] = $aResult['langaddress'];
1559 $aResult['foundorder'] = -$aResult['addressimportance'];
1560 $aSearchResults[$iResNum] = $aResult;
1562 uasort($aSearchResults, 'byImportance');
1564 $aOSMIDDone = array();
1565 $aClassTypeNameDone = array();
1566 $aToFilter = $aSearchResults;
1567 $aSearchResults = array();
1570 foreach($aToFilter as $iResNum => $aResult)
1572 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1573 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1576 $fLat = $aResult['lat'];
1577 $fLon = $aResult['lon'];
1578 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1581 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1582 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1584 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1585 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1586 $aSearchResults[] = $aResult;
1589 // Absolute limit on number of results
1590 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1593 return $aSearchResults;
1602 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1604 $aPoints = explode(',',$_GET['route']);
1605 if (sizeof($aPoints) % 2 != 0)
1607 userError("Uneven number of points");
1610 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1611 $fPrevCoord = false;