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 $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 getViewBoxString()
138 if (!$this->aViewBox) return null;
139 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
142 function setRoute($aRoutePoints)
144 $this->aRoutePoints = $aRoutePoints;
147 function setFeatureType($sFeatureType)
149 switch($sFeatureType)
152 $this->setRankRange(4, 4);
155 $this->setRankRange(8, 8);
158 $this->setRankRange(14, 16);
161 $this->setRankRange(8, 20);
166 function setRankRange($iMin, $iMax)
168 $this->iMinAddressRank = (int)$iMin;
169 $this->iMaxAddressRank = (int)$iMax;
172 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
174 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
177 function setCountryCodesList($aCountryCodes)
179 $this->aCountryCodes = $aCountryCodes;
182 function setQuery($sQueryString)
184 $this->sQuery = $sQueryString;
185 $this->aStructuredQuery = false;
188 function getQueryString()
190 return $this->sQuery;
193 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
195 $this->sQuery = false;
197 $this->aStructuredQuery = array();
198 $this->sAllowedTypesSQLList = '';
200 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sAmentiy, 'amenity', 26, 30, false);
201 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sStreet, 'street', 26, 30, false);
202 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCity, 'city', 14, 24, false);
203 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCounty, 'county', 9, 13, false);
204 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sState, 'state', 8, 8, false);
205 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sCountry, 'country', 4, 4, false);
206 loadStructuredAddressElement($this->aStructuredQuery, $this->iMinAddressRank, $this->iMaxAddressRank, $this->aAddressRankList, $sPostalCode, 'postalcode' , 5, 11, array(5, 11));
208 if (sizeof($this->aStructuredQuery) > 0)
210 $this->sQuery = join(', ', $this->aStructuredQuery);
211 if ($this->iMaxAddressRank < 30)
213 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
219 function getDetails($aPlaceIDs, $iMinAddressRank = 0, $iMaxAddressRank = 30, $aAddressRankList = false, $sAllowedTypesSQLList = false, $bDeDupe = false)
221 if (sizeof($aPlaceIDs) == 0) return array();
223 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
225 // Get the details for display (is this a redundant extra step?)
226 $sPlaceIDs = join(',',$aPlaceIDs);
228 $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,";
229 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
230 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
231 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
232 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
233 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
234 $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, ";
235 $sSQL .= "(extratags->'place') as extra_place ";
236 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
237 $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
238 if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
239 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
241 if ($sAllowedTypesSQLList) $sSQL .= "and placex.class in $sAllowedTypesSQLList ";
242 $sSQL .= "and linked_place_id is null ";
243 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
244 if (!$bDeDupe) $sSQL .= ",place_id";
245 $sSQL .= ",langaddress ";
246 $sSQL .= ",placename ";
248 $sSQL .= ",extratags->'place' ";
250 if (30 >= $iMinAddressRank && 30 <= $iMaxAddressRank)
253 $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,";
254 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
255 $sSQL .= "null as placename,";
256 $sSQL .= "null as ref,";
257 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
258 $sSQL .= "-0.15 as importance, ";
259 $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, ";
260 $sSQL .= "null as extra_place ";
261 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
262 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
263 $sSQL .= "group by place_id";
264 if (!$bDeDupe) $sSQL .= ",place_id";
266 $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,";
267 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
268 $sSQL .= "null as placename,";
269 $sSQL .= "null as ref,";
270 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
271 $sSQL .= "-0.10 as importance, ";
272 $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, ";
273 $sSQL .= "null as extra_place ";
274 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
275 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
276 $sSQL .= "group by place_id";
277 if (!$bDeDupe) $sSQL .= ",place_id";
278 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
281 $sSQL .= "order by importance desc";
282 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
283 $aSearchResults = $this->oDB->getAll($sSQL);
285 if (PEAR::IsError($aSearchResults))
287 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
290 return $aSearchResults;
295 if (!$this->sQuery && !$this->aStructuredQuery) return false;
297 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
299 $sCountryCodesSQL = false;
300 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
302 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
305 // Hack to make it handle "new york, ny" (and variants) correctly
306 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
308 // Conflicts between US state abreviations and various words for 'the' in different languages
309 if (isset($this->aLangPrefOrder['name:en']))
311 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
312 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
313 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
317 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
318 $bBoundingBoxSearch = false;
321 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
322 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
323 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
324 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
325 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
326 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
328 $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)";
329 $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
330 $bBoundingBoxSearch = $this->bBoundedSearch;
334 if ($this->aRoutePoints)
336 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
338 foreach($this->aRouteaPoints as $aPoint)
340 if (!$bFirst) $sViewboxCentreSQL .= ",";
341 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
343 $sViewboxCentreSQL .= ")'::geometry,4326)";
345 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
346 $sViewboxSmallSQL = $this->oDB->getOne($sSQL);
347 if (PEAR::isError($sViewboxSmallSQL))
349 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
351 $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
353 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
354 $sViewboxLargeSQL = $this->oDB->getOne($sSQL);
355 if (PEAR::isError($sViewboxLargeSQL))
357 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
359 $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
360 $bBoundingBoxSearch = $this->bBoundedSearch;
363 // Do we have anything that looks like a lat/lon pair?
364 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
366 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
367 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
368 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
370 $this->setNearPoint(array($fQueryLat, $fQueryLon));
371 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
374 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
376 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
377 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
378 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
380 $this->setNearPoint(array($fQueryLat, $fQueryLon));
381 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
384 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
386 $fQueryLat = $aData[2];
387 $fQueryLon = $aData[3];
388 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
390 $this->setNearPoint(array($fQueryLat, $fQueryLon));
391 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
395 $aSearchResults = array();
396 if ($sQuery || $this->aStructuredQuery)
398 // Start with a blank search
400 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
401 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
402 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
405 // Do we have a radius search?
406 $sNearPointSQL = false;
407 if ($this->aNearPoint)
409 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
410 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
411 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
412 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
415 // Any 'special' terms in the search?
416 $bSpecialTerms = false;
417 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
418 $aSpecialTerms = array();
419 foreach($aSpecialTermsRaw as $aSpecialTerm)
421 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
422 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
425 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
426 $aSpecialTerms = array();
427 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
429 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
430 unset($aStructuredQuery['amenity']);
432 foreach($aSpecialTermsRaw as $aSpecialTerm)
434 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
435 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
436 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
437 $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';
438 if (CONST_Debug) var_Dump($sSQL);
439 $aSearchWords = $this->oDB->getAll($sSQL);
440 $aNewSearches = array();
441 foreach($aSearches as $aSearch)
443 foreach($aSearchWords as $aSearchTerm)
445 $aNewSearch = $aSearch;
446 if ($aSearchTerm['country_code'])
448 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
449 $aNewSearches[] = $aNewSearch;
450 $bSpecialTerms = true;
452 if ($aSearchTerm['class'])
454 $aNewSearch['sClass'] = $aSearchTerm['class'];
455 $aNewSearch['sType'] = $aSearchTerm['type'];
456 $aNewSearches[] = $aNewSearch;
457 $bSpecialTerms = true;
461 $aSearches = $aNewSearches;
464 // Split query into phrases
465 // Commas are used to reduce the search space by indicating where phrases split
466 if ($this->aStructuredQuery)
468 $aPhrases = $this->aStructuredQuery;
469 $bStructuredPhrases = true;
473 $aPhrases = explode(',',$sQuery);
474 $bStructuredPhrases = false;
477 // Convert each phrase to standard form
478 // Create a list of standard words
479 // Get all 'sets' of words
480 // Generate a complete list of all
482 foreach($aPhrases as $iPhrase => $sPhrase)
484 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
485 if (PEAR::isError($aPhrase))
487 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
488 if (CONST_Debug) var_dump($aPhrase);
491 if (trim($aPhrase['string']))
493 $aPhrases[$iPhrase] = $aPhrase;
494 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
495 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
496 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
500 unset($aPhrases[$iPhrase]);
504 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
505 $aPhraseTypes = array_keys($aPhrases);
506 $aPhrases = array_values($aPhrases);
508 if (sizeof($aTokens))
510 // Check which tokens we have, get the ID numbers
511 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
512 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
514 if (CONST_Debug) var_Dump($sSQL);
516 $aValidTokens = array();
517 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
518 else $aDatabaseWords = array();
519 if (PEAR::IsError($aDatabaseWords))
521 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
523 $aPossibleMainWordIDs = array();
524 $aWordFrequencyScores = array();
525 foreach($aDatabaseWords as $aToken)
527 // Very special case - require 2 letter country param to match the country code found
528 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
529 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
534 if (isset($aValidTokens[$aToken['word_token']]))
536 $aValidTokens[$aToken['word_token']][] = $aToken;
540 $aValidTokens[$aToken['word_token']] = array($aToken);
542 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
543 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
545 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
547 // Try and calculate GB postcodes we might be missing
548 foreach($aTokens as $sToken)
550 // Source of gb postcodes is now definitive - always use
551 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
553 if (substr($aData[1],-2,1) != ' ')
555 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
556 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
558 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
559 if ($aGBPostcodeLocation)
561 $aValidTokens[$sToken] = $aGBPostcodeLocation;
564 // US ZIP+4 codes - if there is no token,
565 // merge in the 5-digit ZIP code
566 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
568 if (isset($aValidTokens[$aData[1]]))
570 foreach($aValidTokens[$aData[1]] as $aToken)
572 if (!$aToken['class'])
574 if (isset($aValidTokens[$sToken]))
576 $aValidTokens[$sToken][] = $aToken;
580 $aValidTokens[$sToken] = array($aToken);
588 foreach($aTokens as $sToken)
590 // Unknown single word token with a number - assume it is a house number
591 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
593 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
597 // Any words that have failed completely?
600 // Start the search process
601 $aResultPlaceIDs = array();
604 Calculate all searches using aValidTokens i.e.
605 'Wodsworth Road, Sheffield' =>
609 0 1 (wodsworth)(road)
612 Score how good the search is so they can be ordered
614 foreach($aPhrases as $iPhrase => $sPhrase)
616 $aNewPhraseSearches = array();
617 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
618 else $sPhraseType = '';
620 foreach($aPhrases[$iPhrase]['wordsets'] as $aWordset)
622 $aWordsetSearches = $aSearches;
624 // Add all words from this wordset
625 foreach($aWordset as $iToken => $sToken)
627 //echo "<br><b>$sToken</b>";
628 $aNewWordsetSearches = array();
630 foreach($aWordsetSearches as $aCurrentSearch)
633 //var_dump($aCurrentSearch);
636 // If the token is valid
637 if (isset($aValidTokens[' '.$sToken]))
639 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
641 $aSearch = $aCurrentSearch;
642 $aSearch['iSearchRank']++;
643 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
645 if ($aSearch['sCountryCode'] === false)
647 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
648 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
649 // If reverse order is enabled, it may appear at the beginning as well.
650 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
651 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
653 $aSearch['iSearchRank'] += 5;
655 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
658 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
660 if ($aSearch['fLat'] === '')
662 $aSearch['fLat'] = $aSearchTerm['lat'];
663 $aSearch['fLon'] = $aSearchTerm['lon'];
664 $aSearch['fRadius'] = $aSearchTerm['radius'];
665 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
668 elseif ($sPhraseType == 'postalcode')
670 // 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
671 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
673 // If we already have a name try putting the postcode first
674 if (sizeof($aSearch['aName']))
676 $aNewSearch = $aSearch;
677 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
678 $aNewSearch['aName'] = array();
679 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
680 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
683 if (sizeof($aSearch['aName']))
685 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
687 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
691 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
692 $aSearch['iSearchRank'] += 1000; // skip;
697 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
698 //$aSearch['iNamePhrase'] = $iPhrase;
700 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
704 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
706 if ($aSearch['sHouseNumber'] === '')
708 $aSearch['sHouseNumber'] = $sToken;
709 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
711 // Fall back to not searching for this item (better than nothing)
712 $aSearch = $aCurrentSearch;
713 $aSearch['iSearchRank'] += 1;
714 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
718 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
720 if ($aSearch['sClass'] === '')
722 $aSearch['sOperator'] = $aSearchTerm['operator'];
723 $aSearch['sClass'] = $aSearchTerm['class'];
724 $aSearch['sType'] = $aSearchTerm['type'];
725 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
726 else $aSearch['sOperator'] = 'near'; // near = in for the moment
727 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
729 // Do we have a shortcut id?
730 if ($aSearch['sOperator'] == 'name')
732 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
733 if ($iAmenityID = $this->oDB->getOne($sSQL))
735 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
736 $aSearch['aName'][$iAmenityID] = $iAmenityID;
737 $aSearch['sClass'] = '';
738 $aSearch['sType'] = '';
741 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
744 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
746 if (sizeof($aSearch['aName']))
748 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
750 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
754 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
755 $aSearch['iSearchRank'] += 1000; // skip;
760 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
761 //$aSearch['iNamePhrase'] = $iPhrase;
763 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
767 if (isset($aValidTokens[$sToken]))
769 // Allow searching for a word - but at extra cost
770 foreach($aValidTokens[$sToken] as $aSearchTerm)
772 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
774 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
776 $aSearch = $aCurrentSearch;
777 $aSearch['iSearchRank'] += 1;
778 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
780 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
781 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
783 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
785 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
787 if (empty($aSearchTermToken['country_code'])
788 && empty($aSearchTermToken['lat'])
789 && empty($aSearchTermToken['class']))
791 $aSearch = $aCurrentSearch;
792 $aSearch['iSearchRank'] += 1;
793 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
794 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
800 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
801 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
805 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
807 $aSearch = $aCurrentSearch;
808 $aSearch['iSearchRank'] += 2;
809 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
810 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
811 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
813 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
814 $aSearch['iNamePhrase'] = $iPhrase;
815 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
822 // Allow skipping a word - but at EXTREAM cost
823 //$aSearch = $aCurrentSearch;
824 //$aSearch['iSearchRank']+=100;
825 //$aNewWordsetSearches[] = $aSearch;
829 usort($aNewWordsetSearches, 'bySearchRank');
830 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
832 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
834 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
835 usort($aNewPhraseSearches, 'bySearchRank');
837 $aSearchHash = array();
838 foreach($aNewPhraseSearches as $iSearch => $aSearch)
840 $sHash = serialize($aSearch);
841 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
842 else $aSearchHash[$sHash] = 1;
845 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
848 // Re-group the searches by their score, junk anything over 20 as just not worth trying
849 $aGroupedSearches = array();
850 foreach($aNewPhraseSearches as $aSearch)
852 if ($aSearch['iSearchRank'] < $this->iMaxRank)
854 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
855 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
858 ksort($aGroupedSearches);
861 $aSearches = array();
862 foreach($aGroupedSearches as $iScore => $aNewSearches)
864 $iSearchCount += sizeof($aNewSearches);
865 $aSearches = array_merge($aSearches, $aNewSearches);
866 if ($iSearchCount > 50) break;
869 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
876 // Re-group the searches by their score, junk anything over 20 as just not worth trying
877 $aGroupedSearches = array();
878 foreach($aSearches as $aSearch)
880 if ($aSearch['iSearchRank'] < $this->iMaxRank)
882 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
883 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
886 ksort($aGroupedSearches);
889 if (CONST_Debug) var_Dump($aGroupedSearches);
891 if ($this->bReverseInPlan)
893 $aCopyGroupedSearches = $aGroupedSearches;
894 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
896 foreach($aSearches as $iSearch => $aSearch)
898 if (sizeof($aSearch['aAddress']))
900 $iReverseItem = array_pop($aSearch['aAddress']);
901 if (isset($aPossibleMainWordIDs[$iReverseItem]))
903 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
904 $aSearch['aName'] = array($iReverseItem);
905 $aGroupedSearches[$iGroup][] = $aSearch;
907 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
908 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
914 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
916 $aCopyGroupedSearches = $aGroupedSearches;
917 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
919 foreach($aSearches as $iSearch => $aSearch)
921 $aReductionsList = array($aSearch['aAddress']);
922 $iSearchRank = $aSearch['iSearchRank'];
923 while(sizeof($aReductionsList) > 0)
926 if ($iSearchRank > iMaxRank) break 3;
927 $aNewReductionsList = array();
928 foreach($aReductionsList as $aReductionsWordList)
930 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
932 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
933 $aReverseSearch = $aSearch;
934 $aSearch['aAddress'] = $aReductionsWordListResult;
935 $aSearch['iSearchRank'] = $iSearchRank;
936 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
937 if (sizeof($aReductionsWordListResult) > 0)
939 $aNewReductionsList[] = $aReductionsWordListResult;
943 $aReductionsList = $aNewReductionsList;
947 ksort($aGroupedSearches);
950 // Filter out duplicate searches
951 $aSearchHash = array();
952 foreach($aGroupedSearches as $iGroup => $aSearches)
954 foreach($aSearches as $iSearch => $aSearch)
956 $sHash = serialize($aSearch);
957 if (isset($aSearchHash[$sHash]))
959 unset($aGroupedSearches[$iGroup][$iSearch]);
960 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
964 $aSearchHash[$sHash] = 1;
969 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
973 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
976 foreach($aSearches as $aSearch)
980 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
981 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
984 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
986 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
988 // Just looking for a country by code - look it up
989 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
991 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
992 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
993 $sSQL .= " order by st_area(geometry) desc limit 1";
994 if (CONST_Debug) var_dump($sSQL);
995 $aPlaceIDs = $this->oDB->getCol($sSQL);
1000 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1001 if (!$aSearch['sClass']) continue;
1002 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1003 if ($this->oDB->getOne($sSQL))
1005 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1006 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1007 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
1008 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1009 if (sizeof($this->aExcludePlaceIDs))
1011 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1013 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1014 $sSQL .= " limit $this->iLimit";
1015 if (CONST_Debug) var_dump($sSQL);
1016 $aPlaceIDs = $this->oDB->getCol($sSQL);
1018 // If excluded place IDs are given, it is fair to assume that
1019 // there have been results in the small box, so no further
1020 // expansion in that case.
1021 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1023 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1024 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1025 $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
1026 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1027 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1028 $sSQL .= " limit $this->iLimit";
1029 if (CONST_Debug) var_dump($sSQL);
1030 $aPlaceIDs = $this->oDB->getCol($sSQL);
1035 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1036 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
1037 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1038 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1039 $sSQL .= " limit $this->iLimit";
1040 if (CONST_Debug) var_dump($sSQL);
1041 $aPlaceIDs = $this->oDB->getCol($sSQL);
1047 $aPlaceIDs = array();
1049 // First we need a position, either aName or fLat or both
1053 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1054 // they might be right - but they are just too darned expensive to run
1055 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1056 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1057 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1059 // For infrequent name terms disable index usage for address
1060 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1061 sizeof($aSearch['aName']) == 1 &&
1062 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1064 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1068 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1069 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1072 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1073 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1074 if ($aSearch['fLon'] && $aSearch['fLat'])
1076 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1077 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1079 if (sizeof($this->aExcludePlaceIDs))
1081 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1083 if ($sCountryCodesSQL)
1085 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1088 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
1089 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1091 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1092 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1093 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1094 $aOrder[] = "$sImportanceSQL DESC";
1095 if (sizeof($aSearch['aFullNameAddress']))
1097 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1100 if (sizeof($aTerms))
1102 $sSQL = "select place_id";
1103 $sSQL .= " from search_name";
1104 $sSQL .= " where ".join(' and ',$aTerms);
1105 $sSQL .= " order by ".join(', ',$aOrder);
1106 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1107 $sSQL .= " limit 50";
1108 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1109 $sSQL .= " limit 1";
1111 $sSQL .= " limit ".$this->iLimit;
1113 if (CONST_Debug) { var_dump($sSQL); }
1114 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1115 if (PEAR::IsError($aViewBoxPlaceIDs))
1117 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1119 //var_dump($aViewBoxPlaceIDs);
1120 // Did we have an viewbox matches?
1121 $aPlaceIDs = array();
1122 $bViewBoxMatch = false;
1123 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1125 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1126 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1127 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1128 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1129 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1132 //var_Dump($aPlaceIDs);
1135 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1137 $aRoadPlaceIDs = $aPlaceIDs;
1138 $sPlaceIDs = join(',',$aPlaceIDs);
1140 // Now they are indexed look for a house attached to a street we found
1141 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1142 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1143 if (sizeof($this->aExcludePlaceIDs))
1145 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1147 $sSQL .= " limit $this->iLimit";
1148 if (CONST_Debug) var_dump($sSQL);
1149 $aPlaceIDs = $this->oDB->getCol($sSQL);
1151 // If not try the aux fallback table
1152 if (!sizeof($aPlaceIDs))
1154 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1155 if (sizeof($this->aExcludePlaceIDs))
1157 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1159 //$sSQL .= " limit $this->iLimit";
1160 if (CONST_Debug) var_dump($sSQL);
1161 $aPlaceIDs = $this->oDB->getCol($sSQL);
1164 if (!sizeof($aPlaceIDs))
1166 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1167 if (sizeof($this->aExcludePlaceIDs))
1169 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1171 //$sSQL .= " limit $this->iLimit";
1172 if (CONST_Debug) var_dump($sSQL);
1173 $aPlaceIDs = $this->oDB->getCol($sSQL);
1176 // Fallback to the road
1177 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1179 $aPlaceIDs = $aRoadPlaceIDs;
1184 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1186 $sPlaceIDs = join(',',$aPlaceIDs);
1187 $aClassPlaceIDs = array();
1189 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1191 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1192 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1193 $sSQL .= " and linked_place_id is null";
1194 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1195 $sSQL .= " order by rank_search asc limit $this->iLimit";
1196 if (CONST_Debug) var_dump($sSQL);
1197 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1200 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1202 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1203 $bCacheTable = $this->oDB->getOne($sSQL);
1205 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1207 if (CONST_Debug) var_dump($sSQL);
1208 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1210 // For state / country level searches the normal radius search doesn't work very well
1211 $sPlaceGeom = false;
1212 if ($this->iMaxRank < 9 && $bCacheTable)
1214 // Try and get a polygon to search in instead
1215 $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";
1216 if (CONST_Debug) var_dump($sSQL);
1217 $sPlaceGeom = $this->oDB->getOne($sSQL);
1226 $this->iMaxRank += 5;
1227 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1228 if (CONST_Debug) var_dump($sSQL);
1229 $aPlaceIDs = $this->oDB->getCol($sSQL);
1230 $sPlaceIDs = join(',',$aPlaceIDs);
1233 if ($sPlaceIDs || $sPlaceGeom)
1239 // More efficient - can make the range bigger
1243 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1244 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1245 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1247 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1248 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1251 $sSQL .= ",placex as f where ";
1252 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1257 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1259 if (sizeof($this->aExcludePlaceIDs))
1261 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1263 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1264 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1265 if ($iOffset) $sSQL .= " offset $iOffset";
1266 $sSQL .= " limit $this->iLimit";
1267 if (CONST_Debug) var_dump($sSQL);
1268 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1272 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1275 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1276 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1278 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1279 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1280 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1281 if (sizeof($this->aExcludePlaceIDs))
1283 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1285 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1286 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1287 if ($iOffset) $sSQL .= " offset $iOffset";
1288 $sSQL .= " limit $this->iLimit";
1289 if (CONST_Debug) var_dump($sSQL);
1290 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1295 $aPlaceIDs = $aClassPlaceIDs;
1301 if (PEAR::IsError($aPlaceIDs))
1303 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1306 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1308 foreach($aPlaceIDs as $iPlaceID)
1310 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1312 if ($iQueryLoop > 20) break;
1315 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1317 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1318 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1319 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1320 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1321 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1322 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1323 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1324 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1326 if (CONST_Debug) var_dump($sSQL);
1327 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1331 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1332 if ($iGroupLoop > 4) break;
1333 if ($iQueryLoop > 30) break;
1336 // Did we find anything?
1337 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1339 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1345 // Just interpret as a reverse geocode
1346 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1348 $aSearchResults = $this->getDetails(array($iPlaceID));
1350 $aSearchResults = array();
1354 if (!sizeof($aSearchResults))
1359 $aClassType = getClassTypesWithImportance();
1360 $aRecheckWords = preg_split('/\b/u',$sQuery);
1361 foreach($aRecheckWords as $i => $sWord)
1363 if (!$sWord) unset($aRecheckWords[$i]);
1366 foreach($aSearchResults as $iResNum => $aResult)
1368 if (CONST_Search_AreaPolygons)
1370 // Get the bounding box and outline polygon
1371 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1372 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1373 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1374 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1375 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1376 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1377 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1378 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1379 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1380 $aPointPolygon = $this->oDB->getRow($sSQL);
1381 if (PEAR::IsError($aPointPolygon))
1383 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1386 if ($aPointPolygon['place_id'])
1388 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1389 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1390 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1391 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1393 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1395 $aResult['lat'] = $aPointPolygon['centrelat'];
1396 $aResult['lon'] = $aPointPolygon['centrelon'];
1399 if ($this->bIncludePolygonAsPoints)
1401 // Translate geometary string to point array
1402 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1404 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1406 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1408 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1410 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1413 $iSteps = ($fRadius * 40000)^2;
1414 $fStepSize = (2*pi())/$iSteps;
1415 $aPolyPoints = array();
1416 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1418 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1420 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1421 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1422 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1423 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1427 // Output data suitable for display (points and a bounding box)
1428 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1430 $aResult['aPolyPoints'] = array();
1431 foreach($aPolyPoints as $aPoint)
1433 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1436 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1440 if ($aResult['extra_place'] == 'city')
1442 $aResult['class'] = 'place';
1443 $aResult['type'] = 'city';
1444 $aResult['rank_search'] = 16;
1447 if (!isset($aResult['aBoundingBox']))
1450 $fDiameter = 0.0001;
1452 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1453 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1455 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1457 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1458 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1460 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1462 $fRadius = $fDiameter / 2;
1464 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1465 $fStepSize = (2*pi())/$iSteps;
1466 $aPolyPoints = array();
1467 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1469 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1471 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1472 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1473 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1474 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1476 // Output data suitable for display (points and a bounding box)
1477 if ($this->bIncludePolygonAsPoints)
1479 $aResult['aPolyPoints'] = array();
1480 foreach($aPolyPoints as $aPoint)
1482 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1485 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1488 // Is there an icon set for this type of result?
1489 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1490 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1492 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1495 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1496 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1498 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1500 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1501 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1503 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1506 if ($this->bIncludeAddressDetails)
1508 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1509 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1511 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1515 // Adjust importance for the number of exact string matches in the result
1516 $aResult['importance'] = max(0.001,$aResult['importance']);
1518 $sAddress = $aResult['langaddress'];
1519 foreach($aRecheckWords as $i => $sWord)
1521 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1524 $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
1526 $aResult['name'] = $aResult['langaddress'];
1527 $aResult['foundorder'] = -$aResult['addressimportance'];
1528 $aSearchResults[$iResNum] = $aResult;
1530 uasort($aSearchResults, 'byImportance');
1532 $aOSMIDDone = array();
1533 $aClassTypeNameDone = array();
1534 $aToFilter = $aSearchResults;
1535 $aSearchResults = array();
1538 foreach($aToFilter as $iResNum => $aResult)
1540 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1541 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1544 $fLat = $aResult['lat'];
1545 $fLon = $aResult['lon'];
1546 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1549 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1550 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1552 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1553 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1554 $aSearchResults[] = $aResult;
1557 // Absolute limit on number of results
1558 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1561 return $aSearchResults;
1570 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1572 $aPoints = explode(',',$_GET['route']);
1573 if (sizeof($aPoints) % 2 != 0)
1575 userError("Uneven number of points");
1578 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1579 $fPrevCoord = false;