6 protected $aLangPrefOrder = array();
8 protected $bIncludeAddressDetails = false;
10 protected $bIncludePolygonAsPoints = false;
11 protected $bIncludePolygonAsText = false;
12 protected $bIncludePolygonAsGeoJSON = false;
13 protected $bIncludePolygonAsKML = false;
14 protected $bIncludePolygonAsSVG = false;
16 protected $aExcludePlaceIDs = array();
17 protected $bDeDupe = true;
18 protected $bReverseInPlan = false;
20 protected $iLimit = 20;
21 protected $iFinalLimit = 10;
22 protected $iOffset = 0;
24 protected $aCountryCodes = false;
25 protected $aNearPoint = false;
27 protected $bBoundedSearch = false;
28 protected $aViewBox = false;
29 protected $aRoutePoints = false;
31 protected $iMaxRank = 20;
32 protected $iMinAddressRank = 0;
33 protected $iMaxAddressRank = 30;
34 protected $aAddressRankList = array();
36 protected $sAllowedTypesSQLList = false;
38 protected $sQuery = false;
39 protected $aStructuredQuery = false;
41 function Geocode(&$oDB)
46 function setLanguagePreference($aLangPref)
48 $this->aLangPrefOrder = $aLangPref;
51 function setIncludeAddressDetails($bAddressDetails = true)
53 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
56 function getIncludeAddressDetails()
58 return $this->bIncludeAddressDetails;
61 function setIncludePolygonAsPoints($b = true)
63 $this->bIncludePolygonAsPoints = $b;
66 function getIncludePolygonAsPoints()
68 return $this->bIncludePolygonAsPoints;
71 function setIncludePolygonAsText($b = true)
73 $this->bIncludePolygonAsText = $b;
76 function getIncludePolygonAsText()
78 return $this->bIncludePolygonAsText;
81 function setIncludePolygonAsGeoJSON($b = true)
83 $this->bIncludePolygonAsGeoJSON = $b;
86 function setIncludePolygonAsKML($b = true)
88 $this->bIncludePolygonAsKML = $b;
91 function setIncludePolygonAsSVG($b = true)
93 $this->bIncludePolygonAsSVG = $b;
96 function setDeDupe($bDeDupe = true)
98 $this->bDeDupe = (bool)$bDeDupe;
101 function setLimit($iLimit = 10)
103 if ($iLimit > 50) $iLimit = 50;
104 if ($iLimit < 1) $iLimit = 1;
106 $this->iFinalLimit = $iLimit;
107 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
110 function setOffset($iOffset = 0)
112 $this->iOffset = $iOffset;
115 function setExcludedPlaceIDs($a)
117 // TODO: force to int
118 $this->aExcludePlaceIDs = $a;
121 function getExcludedPlaceIDs()
123 return $this->aExcludePlaceIDs;
126 function setBounded($bBoundedSearch = true)
128 $this->bBoundedSearch = (bool)$bBoundedSearch;
131 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
133 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
136 function setRoute($aRoutePoints)
138 $this->aRoutePoints = $aRoutePoints;
141 function setFeatureType($sFeatureType)
143 switch($sFeatureType)
146 $this->setRankRange(4, 4);
149 $this->setRankRange(8, 8);
152 $this->setRankRange(14, 16);
155 $this->setRankRange(8, 20);
160 function setRankRange($iMin, $iMax)
162 $this->iMinAddressRank = (int)$iMin;
163 $this->iMaxAddressRank = (int)$iMax;
166 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
168 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
171 function setCountryCodesList($aCountryCodes)
173 $this->aCountryCodes = $aCountryCodes;
176 function setQuery($sQueryString)
178 $this->sQuery = $sQueryString;
179 $this->aStructuredQuery = false;
182 function getQueryString()
184 return $this->sQuery;
187 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
189 $this->sQuery = false;
191 $this->aStructuredQuery = array();
192 $this->sAllowedTypesSQLList = '';
194 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sAmentiy, 'amenity', 26, 30, false);
195 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sStreet, 'street', 26, 30, false);
196 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCity, 'city', 14, 24, false);
197 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCounty, 'county', 9, 13, false);
198 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sState, 'state', 8, 8, false);
199 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCountry, 'country', 4, 4, false);
200 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sPostalCode, 'postalcode' , 5, 11, array(5, 11));
202 if (sizeof($this->aStructuredQuery) > 0)
204 $this->sQuery = join(', ', $this->aStructuredQuery);
205 if ($this->iMaxAddressRank < 30)
207 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
213 function getDetails($aPlaceIDs, $iMinAddressRank = 0, $iMaxAddressRank = 30, $aAddressRankList = false, $sAllowedTypesSQLList = false, $bDeDupe = false)
215 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
217 // Get the details for display (is this a redundant extra step?)
218 $sPlaceIDs = join(',',$aPlaceIDs);
220 $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,";
221 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
222 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
223 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
224 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
225 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
226 $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, ";
227 $sSQL .= "(extratags->'place') as extra_place ";
228 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
229 $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
230 if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
231 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
233 if ($sAllowedTypesSQLList) $sSQL .= "and placex.class in $sAllowedTypesSQLList ";
234 $sSQL .= "and linked_place_id is null ";
235 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
236 if (!$bDeDupe) $sSQL .= ",place_id";
237 $sSQL .= ",langaddress ";
238 $sSQL .= ",placename ";
240 $sSQL .= ",extratags->'place' ";
242 if (30 >= $iMinAddressRank && 30 <= $iMaxAddressRank)
245 $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,";
246 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
247 $sSQL .= "null as placename,";
248 $sSQL .= "null as ref,";
249 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
250 $sSQL .= "-0.15 as importance, ";
251 $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, ";
252 $sSQL .= "null as extra_place ";
253 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
254 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
255 $sSQL .= "group by place_id";
256 if (!$bDeDupe) $sSQL .= ",place_id";
258 $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,";
259 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
260 $sSQL .= "null as placename,";
261 $sSQL .= "null as ref,";
262 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
263 $sSQL .= "-0.10 as importance, ";
264 $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, ";
265 $sSQL .= "null as extra_place ";
266 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
267 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
268 $sSQL .= "group by place_id";
269 if (!$bDeDupe) $sSQL .= ",place_id";
270 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
273 $sSQL .= "order by importance desc";
274 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
275 $aSearchResults = $this->oDB->getAll($sSQL);
277 if (PEAR::IsError($aSearchResults))
279 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
282 return $aSearchResults;
287 if (!$this->sQuery && !$this->aStructuredQuery) return false;
289 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
291 $sCountryCodesSQL = false;
292 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
294 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
297 // Hack to make it handle "new york, ny" (and variants) correctly
298 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
300 // Conflicts between US state abreviations and various words for 'the' in different languages
301 if (isset($this->aLangPrefOrder['name:en']))
303 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
304 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
305 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
309 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
310 $bBoundingBoxSearch = false;
313 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
314 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
315 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
316 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
317 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
318 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
320 $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)";
321 $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
322 $bBoundingBoxSearch = $this->bBoundedSearch;
326 if ($this->aRoutePoints)
328 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
330 foreach($this->aRouteaPoints as $aPoint)
332 if (!$bFirst) $sViewboxCentreSQL .= ",";
333 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
335 $sViewboxCentreSQL .= ")'::geometry,4326)";
337 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
338 $sViewboxSmallSQL = $this->oDB->getOne($sSQL);
339 if (PEAR::isError($sViewboxSmallSQL))
341 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
343 $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
345 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
346 $sViewboxLargeSQL = $this->oDB->getOne($sSQL);
347 if (PEAR::isError($sViewboxLargeSQL))
349 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
351 $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
352 $bBoundingBoxSearch = $this->bBoundedSearch;
355 // Do we have anything that looks like a lat/lon pair?
356 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
358 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
359 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
360 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
362 $this->setNearPoint(array($fQueryLat, $fQueryLon));
363 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
366 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
368 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
369 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
370 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
372 $this->setNearPoint(array($fQueryLat, $fQueryLon));
373 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
376 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
378 $fQueryLat = $aData[2];
379 $fQueryLon = $aData[3];
380 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
382 $this->setNearPoint(array($fQueryLat, $fQueryLon));
383 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
387 $aSearchResults = array();
388 if ($sQuery || $aStructuredQuery)
390 // Start with a blank search
392 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
393 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
394 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
397 // Do we have a radius search?
398 $sNearPointSQL = false;
399 if ($this->aNearPoint)
401 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
402 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
403 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
404 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
407 // Any 'special' terms in the search?
408 $bSpecialTerms = false;
409 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
410 $aSpecialTerms = array();
411 foreach($aSpecialTermsRaw as $aSpecialTerm)
413 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
414 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
417 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
418 $aSpecialTerms = array();
419 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
421 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
422 unset($aStructuredQuery['amenity']);
424 foreach($aSpecialTermsRaw as $aSpecialTerm)
426 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
427 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
428 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
429 $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';
430 if (CONST_Debug) var_Dump($sSQL);
431 $aSearchWords = $this->oDB->getAll($sSQL);
432 $aNewSearches = array();
433 foreach($aSearches as $aSearch)
435 foreach($aSearchWords as $aSearchTerm)
437 $aNewSearch = $aSearch;
438 if ($aSearchTerm['country_code'])
440 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
441 $aNewSearches[] = $aNewSearch;
442 $bSpecialTerms = true;
444 if ($aSearchTerm['class'])
446 $aNewSearch['sClass'] = $aSearchTerm['class'];
447 $aNewSearch['sType'] = $aSearchTerm['type'];
448 $aNewSearches[] = $aNewSearch;
449 $bSpecialTerms = true;
453 $aSearches = $aNewSearches;
456 // Split query into phrases
457 // Commas are used to reduce the search space by indicating where phrases split
458 if ($this->aStructuredQuery)
460 $aPhrases = $aStructuredQuery;
461 $bStructuredPhrases = true;
465 $aPhrases = explode(',',$sQuery);
466 $bStructuredPhrases = false;
469 // Convert each phrase to standard form
470 // Create a list of standard words
471 // Get all 'sets' of words
472 // Generate a complete list of all
474 foreach($aPhrases as $iPhrase => $sPhrase)
476 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
477 if (PEAR::isError($aPhrase))
479 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
480 if (CONST_Debug) var_dump($aPhrase);
483 if (trim($aPhrase['string']))
485 $aPhrases[$iPhrase] = $aPhrase;
486 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
487 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
488 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
492 unset($aPhrases[$iPhrase]);
496 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
497 $aPhraseTypes = array_keys($aPhrases);
498 $aPhrases = array_values($aPhrases);
500 if (sizeof($aTokens))
502 // Check which tokens we have, get the ID numbers
503 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
504 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
506 if (CONST_Debug) var_Dump($sSQL);
508 $aValidTokens = array();
509 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
510 else $aDatabaseWords = array();
511 if (PEAR::IsError($aDatabaseWords))
513 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
515 $aPossibleMainWordIDs = array();
516 $aWordFrequencyScores = array();
517 foreach($aDatabaseWords as $aToken)
519 // Very special case - require 2 letter country param to match the country code found
520 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
521 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
526 if (isset($aValidTokens[$aToken['word_token']]))
528 $aValidTokens[$aToken['word_token']][] = $aToken;
532 $aValidTokens[$aToken['word_token']] = array($aToken);
534 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
535 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
537 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
539 // Try and calculate GB postcodes we might be missing
540 foreach($aTokens as $sToken)
542 // Source of gb postcodes is now definitive - always use
543 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
545 if (substr($aData[1],-2,1) != ' ')
547 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
548 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
550 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
551 if ($aGBPostcodeLocation)
553 $aValidTokens[$sToken] = $aGBPostcodeLocation;
556 // US ZIP+4 codes - if there is no token,
557 // merge in the 5-digit ZIP code
558 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
560 if (isset($aValidTokens[$aData[1]]))
562 foreach($aValidTokens[$aData[1]] as $aToken)
564 if (!$aToken['class'])
566 if (isset($aValidTokens[$sToken]))
568 $aValidTokens[$sToken][] = $aToken;
572 $aValidTokens[$sToken] = array($aToken);
580 foreach($aTokens as $sToken)
582 // Unknown single word token with a number - assume it is a house number
583 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
585 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
589 // Any words that have failed completely?
592 // Start the search process
593 $aResultPlaceIDs = array();
596 Calculate all searches using aValidTokens i.e.
597 'Wodsworth Road, Sheffield' =>
601 0 1 (wodsworth)(road)
604 Score how good the search is so they can be ordered
606 foreach($aPhrases as $iPhrase => $sPhrase)
608 $aNewPhraseSearches = array();
609 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
610 else $sPhraseType = '';
612 foreach($aPhrases[$iPhrase]['wordsets'] as $aWordset)
614 $aWordsetSearches = $aSearches;
616 // Add all words from this wordset
617 foreach($aWordset as $iToken => $sToken)
619 //echo "<br><b>$sToken</b>";
620 $aNewWordsetSearches = array();
622 foreach($aWordsetSearches as $aCurrentSearch)
625 //var_dump($aCurrentSearch);
628 // If the token is valid
629 if (isset($aValidTokens[' '.$sToken]))
631 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
633 $aSearch = $aCurrentSearch;
634 $aSearch['iSearchRank']++;
635 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
637 if ($aSearch['sCountryCode'] === false)
639 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
640 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
641 // If reverse order is enabled, it may appear at the beginning as well.
642 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
643 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
645 $aSearch['iSearchRank'] += 5;
647 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
650 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
652 if ($aSearch['fLat'] === '')
654 $aSearch['fLat'] = $aSearchTerm['lat'];
655 $aSearch['fLon'] = $aSearchTerm['lon'];
656 $aSearch['fRadius'] = $aSearchTerm['radius'];
657 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
660 elseif ($sPhraseType == 'postalcode')
662 // 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
663 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
665 // If we already have a name try putting the postcode first
666 if (sizeof($aSearch['aName']))
668 $aNewSearch = $aSearch;
669 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
670 $aNewSearch['aName'] = array();
671 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
672 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
675 if (sizeof($aSearch['aName']))
677 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
679 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
683 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
684 $aSearch['iSearchRank'] += 1000; // skip;
689 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
690 //$aSearch['iNamePhrase'] = $iPhrase;
692 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
696 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
698 if ($aSearch['sHouseNumber'] === '')
700 $aSearch['sHouseNumber'] = $sToken;
701 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
703 // Fall back to not searching for this item (better than nothing)
704 $aSearch = $aCurrentSearch;
705 $aSearch['iSearchRank'] += 1;
706 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
710 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
712 if ($aSearch['sClass'] === '')
714 $aSearch['sOperator'] = $aSearchTerm['operator'];
715 $aSearch['sClass'] = $aSearchTerm['class'];
716 $aSearch['sType'] = $aSearchTerm['type'];
717 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
718 else $aSearch['sOperator'] = 'near'; // near = in for the moment
720 // Do we have a shortcut id?
721 if ($aSearch['sOperator'] == 'name')
723 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
724 if ($iAmenityID = $this->oDB->getOne($sSQL))
726 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
727 $aSearch['aName'][$iAmenityID] = $iAmenityID;
728 $aSearch['sClass'] = '';
729 $aSearch['sType'] = '';
732 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
735 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
737 if (sizeof($aSearch['aName']))
739 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
741 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
745 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
746 $aSearch['iSearchRank'] += 1000; // skip;
751 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
752 //$aSearch['iNamePhrase'] = $iPhrase;
754 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
758 if (isset($aValidTokens[$sToken]))
760 // Allow searching for a word - but at extra cost
761 foreach($aValidTokens[$sToken] as $aSearchTerm)
763 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
765 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
767 $aSearch = $aCurrentSearch;
768 $aSearch['iSearchRank'] += 1;
769 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
771 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
772 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
774 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
776 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
778 if (empty($aSearchTermToken['country_code'])
779 && empty($aSearchTermToken['lat'])
780 && empty($aSearchTermToken['class']))
782 $aSearch = $aCurrentSearch;
783 $aSearch['iSearchRank'] += 1;
784 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
785 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
791 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
792 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
796 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
798 $aSearch = $aCurrentSearch;
799 $aSearch['iSearchRank'] += 2;
800 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
801 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
802 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
804 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
805 $aSearch['iNamePhrase'] = $iPhrase;
806 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
813 // Allow skipping a word - but at EXTREAM cost
814 //$aSearch = $aCurrentSearch;
815 //$aSearch['iSearchRank']+=100;
816 //$aNewWordsetSearches[] = $aSearch;
820 usort($aNewWordsetSearches, 'bySearchRank');
821 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
823 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
825 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
826 usort($aNewPhraseSearches, 'bySearchRank');
828 $aSearchHash = array();
829 foreach($aNewPhraseSearches as $iSearch => $aSearch)
831 $sHash = serialize($aSearch);
832 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
833 else $aSearchHash[$sHash] = 1;
836 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
839 // Re-group the searches by their score, junk anything over 20 as just not worth trying
840 $aGroupedSearches = array();
841 foreach($aNewPhraseSearches as $aSearch)
843 if ($aSearch['iSearchRank'] < $this->iMaxRank)
845 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
846 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
849 ksort($aGroupedSearches);
852 $aSearches = array();
853 foreach($aGroupedSearches as $iScore => $aNewSearches)
855 $iSearchCount += sizeof($aNewSearches);
856 $aSearches = array_merge($aSearches, $aNewSearches);
857 if ($iSearchCount > 50) break;
860 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
867 // Re-group the searches by their score, junk anything over 20 as just not worth trying
868 $aGroupedSearches = array();
869 foreach($aSearches as $aSearch)
871 if ($aSearch['iSearchRank'] < $this->iMaxRank)
873 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
874 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
877 ksort($aGroupedSearches);
880 if (CONST_Debug) var_Dump($aGroupedSearches);
882 if ($this->bReverseInPlan)
884 $aCopyGroupedSearches = $aGroupedSearches;
885 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
887 foreach($aSearches as $iSearch => $aSearch)
889 if (sizeof($aSearch['aAddress']))
891 $iReverseItem = array_pop($aSearch['aAddress']);
892 if (isset($aPossibleMainWordIDs[$iReverseItem]))
894 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
895 $aSearch['aName'] = array($iReverseItem);
896 $aGroupedSearches[$iGroup][] = $aSearch;
898 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
899 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
905 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
907 $aCopyGroupedSearches = $aGroupedSearches;
908 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
910 foreach($aSearches as $iSearch => $aSearch)
912 $aReductionsList = array($aSearch['aAddress']);
913 $iSearchRank = $aSearch['iSearchRank'];
914 while(sizeof($aReductionsList) > 0)
917 if ($iSearchRank > iMaxRank) break 3;
918 $aNewReductionsList = array();
919 foreach($aReductionsList as $aReductionsWordList)
921 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
923 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
924 $aReverseSearch = $aSearch;
925 $aSearch['aAddress'] = $aReductionsWordListResult;
926 $aSearch['iSearchRank'] = $iSearchRank;
927 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
928 if (sizeof($aReductionsWordListResult) > 0)
930 $aNewReductionsList[] = $aReductionsWordListResult;
934 $aReductionsList = $aNewReductionsList;
938 ksort($aGroupedSearches);
941 // Filter out duplicate searches
942 $aSearchHash = array();
943 foreach($aGroupedSearches as $iGroup => $aSearches)
945 foreach($aSearches as $iSearch => $aSearch)
947 $sHash = serialize($aSearch);
948 if (isset($aSearchHash[$sHash]))
950 unset($aGroupedSearches[$iGroup][$iSearch]);
951 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
955 $aSearchHash[$sHash] = 1;
960 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
964 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
967 foreach($aSearches as $aSearch)
971 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
972 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
975 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
977 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
979 // Just looking for a country by code - look it up
980 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
982 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
983 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
984 $sSQL .= " order by st_area(geometry) desc limit 1";
985 if (CONST_Debug) var_dump($sSQL);
986 $aPlaceIDs = $this->oDB->getCol($sSQL);
991 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
992 if (!$aSearch['sClass']) continue;
993 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
994 if ($this->oDB->getOne($sSQL))
996 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
997 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
998 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
999 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1000 if (sizeof($this->aExcludePlaceIDs))
1002 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1004 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1005 $sSQL .= " limit $this->iLimit";
1006 if (CONST_Debug) var_dump($sSQL);
1007 $aPlaceIDs = $this->oDB->getCol($sSQL);
1009 // If excluded place IDs are given, it is fair to assume that
1010 // there have been results in the small box, so no further
1011 // expansion in that case.
1012 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1014 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1015 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1016 $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
1017 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1018 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1019 $sSQL .= " limit $this->iLimit";
1020 if (CONST_Debug) var_dump($sSQL);
1021 $aPlaceIDs = $this->oDB->getCol($sSQL);
1026 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1027 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
1028 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1029 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1030 $sSQL .= " limit $this->iLimit";
1031 if (CONST_Debug) var_dump($sSQL);
1032 $aPlaceIDs = $this->oDB->getCol($sSQL);
1038 $aPlaceIDs = array();
1040 // First we need a position, either aName or fLat or both
1044 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1045 // they might be right - but they are just too darned expensive to run
1046 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1047 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1048 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1050 // For infrequent name terms disable index usage for address
1051 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1052 sizeof($aSearch['aName']) == 1 &&
1053 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1055 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1059 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1060 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1063 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1064 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1065 if ($aSearch['fLon'] && $aSearch['fLat'])
1067 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1068 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1070 if (sizeof($this->aExcludePlaceIDs))
1072 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1074 if ($sCountryCodesSQL)
1076 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1079 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
1080 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1082 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1083 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1084 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1085 $aOrder[] = "$sImportanceSQL DESC";
1086 if (sizeof($aSearch['aFullNameAddress']))
1088 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1091 if (sizeof($aTerms))
1093 $sSQL = "select place_id";
1094 $sSQL .= " from search_name";
1095 $sSQL .= " where ".join(' and ',$aTerms);
1096 $sSQL .= " order by ".join(', ',$aOrder);
1097 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1098 $sSQL .= " limit 50";
1099 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1100 $sSQL .= " limit 1";
1102 $sSQL .= " limit ".$this->iLimit;
1104 if (CONST_Debug) { var_dump($sSQL); }
1105 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1106 if (PEAR::IsError($aViewBoxPlaceIDs))
1108 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1110 //var_dump($aViewBoxPlaceIDs);
1111 // Did we have an viewbox matches?
1112 $aPlaceIDs = array();
1113 $bViewBoxMatch = false;
1114 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1116 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1117 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1118 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1119 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1120 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1123 //var_Dump($aPlaceIDs);
1126 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1128 $aRoadPlaceIDs = $aPlaceIDs;
1129 $sPlaceIDs = join(',',$aPlaceIDs);
1131 // Now they are indexed look for a house attached to a street we found
1132 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1133 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1134 if (sizeof($this->aExcludePlaceIDs))
1136 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1138 $sSQL .= " limit $this->iLimit";
1139 if (CONST_Debug) var_dump($sSQL);
1140 $aPlaceIDs = $this->oDB->getCol($sSQL);
1142 // If not try the aux fallback table
1143 if (!sizeof($aPlaceIDs))
1145 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1146 if (sizeof($this->aExcludePlaceIDs))
1148 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1150 //$sSQL .= " limit $this->iLimit";
1151 if (CONST_Debug) var_dump($sSQL);
1152 $aPlaceIDs = $this->oDB->getCol($sSQL);
1155 if (!sizeof($aPlaceIDs))
1157 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1158 if (sizeof($this->aExcludePlaceIDs))
1160 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1162 //$sSQL .= " limit $this->iLimit";
1163 if (CONST_Debug) var_dump($sSQL);
1164 $aPlaceIDs = $this->oDB->getCol($sSQL);
1167 // Fallback to the road
1168 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1170 $aPlaceIDs = $aRoadPlaceIDs;
1175 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1177 $sPlaceIDs = join(',',$aPlaceIDs);
1178 $aClassPlaceIDs = array();
1180 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1182 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1183 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1184 $sSQL .= " and linked_place_id is null";
1185 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1186 $sSQL .= " order by rank_search asc limit $this->iLimit";
1187 if (CONST_Debug) var_dump($sSQL);
1188 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1191 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1193 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1194 $bCacheTable = $this->oDB->getOne($sSQL);
1196 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1198 if (CONST_Debug) var_dump($sSQL);
1199 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1201 // For state / country level searches the normal radius search doesn't work very well
1202 $sPlaceGeom = false;
1203 if ($this->iMaxRank < 9 && $bCacheTable)
1205 // Try and get a polygon to search in instead
1206 $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";
1207 if (CONST_Debug) var_dump($sSQL);
1208 $sPlaceGeom = $this->oDB->getOne($sSQL);
1217 $this->iMaxRank += 5;
1218 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1219 if (CONST_Debug) var_dump($sSQL);
1220 $aPlaceIDs = $this->oDB->getCol($sSQL);
1221 $sPlaceIDs = join(',',$aPlaceIDs);
1224 if ($sPlaceIDs || $sPlaceGeom)
1230 // More efficient - can make the range bigger
1234 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1235 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1236 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1238 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1239 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1242 $sSQL .= ",placex as f where ";
1243 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1248 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1250 if (sizeof($this->aExcludePlaceIDs))
1252 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1254 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1255 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1256 if ($iOffset) $sSQL .= " offset $iOffset";
1257 $sSQL .= " limit $this->iLimit";
1258 if (CONST_Debug) var_dump($sSQL);
1259 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1263 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1266 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1267 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1269 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1270 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1271 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1272 if (sizeof($this->aExcludePlaceIDs))
1274 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1276 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1277 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1278 if ($iOffset) $sSQL .= " offset $iOffset";
1279 $sSQL .= " limit $this->iLimit";
1280 if (CONST_Debug) var_dump($sSQL);
1281 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1286 $aPlaceIDs = $aClassPlaceIDs;
1292 if (PEAR::IsError($aPlaceIDs))
1294 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1297 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1299 foreach($aPlaceIDs as $iPlaceID)
1301 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1303 if ($iQueryLoop > 20) break;
1306 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1308 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1309 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1310 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1311 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1312 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1313 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1314 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1315 if ($aAddressRankList) $sSQL .= " OR 30 in (".join(',',$aAddressRankList).")";
1317 if (CONST_Debug) var_dump($sSQL);
1318 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1322 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1323 if ($iGroupLoop > 4) break;
1324 if ($iQueryLoop > 30) break;
1327 // Did we find anything?
1328 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1330 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1336 // Just interpret as a reverse geocode
1337 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1338 $aSearchResults = $this->getDetails(array($iPlaceID));
1342 if (!sizeof($aSearchResults))
1347 $aClassType = getClassTypesWithImportance();
1348 $aRecheckWords = preg_split('/\b/u',$sQuery);
1349 foreach($aRecheckWords as $i => $sWord)
1351 if (!$sWord) unset($aRecheckWords[$i]);
1354 foreach($aSearchResults as $iResNum => $aResult)
1356 if (CONST_Search_AreaPolygons)
1358 // Get the bounding box and outline polygon
1359 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1360 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1361 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1362 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1363 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1364 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1365 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1366 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1367 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1368 $aPointPolygon = $this->oDB->getRow($sSQL);
1369 if (PEAR::IsError($aPointPolygon))
1371 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1374 if ($aPointPolygon['place_id'])
1376 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1377 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1378 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1379 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1381 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1383 $aResult['lat'] = $aPointPolygon['centrelat'];
1384 $aResult['lon'] = $aPointPolygon['centrelon'];
1386 if ($this->bIncludePolygonAsPoints)
1388 // Translate geometary string to point array
1389 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1391 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1393 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1395 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1397 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1400 $iSteps = ($fRadius * 40000)^2;
1401 $fStepSize = (2*pi())/$iSteps;
1402 $aPolyPoints = array();
1403 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1405 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1407 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1408 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1409 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1410 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1414 // Output data suitable for display (points and a bounding box)
1415 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1417 $aResult['aPolyPoints'] = array();
1418 foreach($aPolyPoints as $aPoint)
1420 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1423 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1427 if ($aResult['extra_place'] == 'city')
1429 $aResult['class'] = 'place';
1430 $aResult['type'] = 'city';
1431 $aResult['rank_search'] = 16;
1434 if (!isset($aResult['aBoundingBox']))
1437 $fDiameter = 0.0001;
1439 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1440 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1442 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1444 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1445 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1447 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1449 $fRadius = $fDiameter / 2;
1451 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1452 $fStepSize = (2*pi())/$iSteps;
1453 $aPolyPoints = array();
1454 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1456 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1458 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1459 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1460 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1461 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1463 // Output data suitable for display (points and a bounding box)
1464 if ($this->bIncludePolygonAsPoints)
1466 $aResult['aPolyPoints'] = array();
1467 foreach($aPolyPoints as $aPoint)
1469 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1472 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1475 // Is there an icon set for this type of result?
1476 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1477 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1479 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1482 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1483 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1485 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1488 if ($this->bIncludeAddressDetails)
1490 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1491 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1493 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1497 // Adjust importance for the number of exact string matches in the result
1498 $aResult['importance'] = max(0.001,$aResult['importance']);
1500 $sAddress = $aResult['langaddress'];
1501 foreach($aRecheckWords as $i => $sWord)
1503 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1506 $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
1508 $aResult['name'] = $aResult['langaddress'];
1509 $aResult['foundorder'] = -$aResult['addressimportance'];
1510 $aSearchResults[$iResNum] = $aResult;
1512 uasort($aSearchResults, 'byImportance');
1514 $aOSMIDDone = array();
1515 $aClassTypeNameDone = array();
1516 $aToFilter = $aSearchResults;
1517 $aSearchResults = array();
1520 foreach($aToFilter as $iResNum => $aResult)
1522 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1523 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1526 $fLat = $aResult['lat'];
1527 $fLon = $aResult['lon'];
1528 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1531 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1532 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1534 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1535 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1536 $aSearchResults[] = $aResult;
1539 // Absolute limit on number of results
1540 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1543 return $aSearchResults;
1552 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1554 $aPoints = explode(',',$_GET['route']);
1555 if (sizeof($aPoints) % 2 != 0)
1557 userError("Uneven number of points");
1560 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1561 $fPrevCoord = false;