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 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 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
223 // Get the details for display (is this a redundant extra step?)
224 $sPlaceIDs = join(',',$aPlaceIDs);
226 $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,";
227 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
228 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
229 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
230 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
231 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
232 $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, ";
233 $sSQL .= "(extratags->'place') as extra_place ";
234 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
235 $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
236 if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
237 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
239 if ($sAllowedTypesSQLList) $sSQL .= "and placex.class in $sAllowedTypesSQLList ";
240 $sSQL .= "and linked_place_id is null ";
241 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
242 if (!$bDeDupe) $sSQL .= ",place_id";
243 $sSQL .= ",langaddress ";
244 $sSQL .= ",placename ";
246 $sSQL .= ",extratags->'place' ";
248 if (30 >= $iMinAddressRank && 30 <= $iMaxAddressRank)
251 $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,";
252 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
253 $sSQL .= "null as placename,";
254 $sSQL .= "null as ref,";
255 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
256 $sSQL .= "-0.15 as importance, ";
257 $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, ";
258 $sSQL .= "null as extra_place ";
259 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
260 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
261 $sSQL .= "group by place_id";
262 if (!$bDeDupe) $sSQL .= ",place_id";
264 $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,";
265 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
266 $sSQL .= "null as placename,";
267 $sSQL .= "null as ref,";
268 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
269 $sSQL .= "-0.10 as importance, ";
270 $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, ";
271 $sSQL .= "null as extra_place ";
272 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
273 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
274 $sSQL .= "group by place_id";
275 if (!$bDeDupe) $sSQL .= ",place_id";
276 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
279 $sSQL .= "order by importance desc";
280 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
281 $aSearchResults = $this->oDB->getAll($sSQL);
283 if (PEAR::IsError($aSearchResults))
285 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
288 return $aSearchResults;
293 if (!$this->sQuery && !$this->aStructuredQuery) return false;
295 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
297 $sCountryCodesSQL = false;
298 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
300 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
303 // Hack to make it handle "new york, ny" (and variants) correctly
304 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
306 // Conflicts between US state abreviations and various words for 'the' in different languages
307 if (isset($this->aLangPrefOrder['name:en']))
309 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
310 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
311 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
315 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
316 $bBoundingBoxSearch = false;
319 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
320 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
321 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
322 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
323 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
324 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
326 $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)";
327 $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
328 $bBoundingBoxSearch = $this->bBoundedSearch;
332 if ($this->aRoutePoints)
334 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
336 foreach($this->aRouteaPoints as $aPoint)
338 if (!$bFirst) $sViewboxCentreSQL .= ",";
339 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
341 $sViewboxCentreSQL .= ")'::geometry,4326)";
343 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
344 $sViewboxSmallSQL = $this->oDB->getOne($sSQL);
345 if (PEAR::isError($sViewboxSmallSQL))
347 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
349 $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
351 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
352 $sViewboxLargeSQL = $this->oDB->getOne($sSQL);
353 if (PEAR::isError($sViewboxLargeSQL))
355 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
357 $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
358 $bBoundingBoxSearch = $this->bBoundedSearch;
361 // Do we have anything that looks like a lat/lon pair?
362 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
364 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
365 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
366 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
368 $this->setNearPoint(array($fQueryLat, $fQueryLon));
369 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
372 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
374 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
375 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
376 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
378 $this->setNearPoint(array($fQueryLat, $fQueryLon));
379 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
382 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
384 $fQueryLat = $aData[2];
385 $fQueryLon = $aData[3];
386 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
388 $this->setNearPoint(array($fQueryLat, $fQueryLon));
389 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
393 $aSearchResults = array();
394 if ($sQuery || $aStructuredQuery)
396 // Start with a blank search
398 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
399 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
400 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
403 // Do we have a radius search?
404 $sNearPointSQL = false;
405 if ($this->aNearPoint)
407 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
408 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
409 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
410 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
413 // Any 'special' terms in the search?
414 $bSpecialTerms = false;
415 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
416 $aSpecialTerms = array();
417 foreach($aSpecialTermsRaw as $aSpecialTerm)
419 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
420 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
423 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
424 $aSpecialTerms = array();
425 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
427 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
428 unset($aStructuredQuery['amenity']);
430 foreach($aSpecialTermsRaw as $aSpecialTerm)
432 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
433 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
434 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
435 $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';
436 if (CONST_Debug) var_Dump($sSQL);
437 $aSearchWords = $this->oDB->getAll($sSQL);
438 $aNewSearches = array();
439 foreach($aSearches as $aSearch)
441 foreach($aSearchWords as $aSearchTerm)
443 $aNewSearch = $aSearch;
444 if ($aSearchTerm['country_code'])
446 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
447 $aNewSearches[] = $aNewSearch;
448 $bSpecialTerms = true;
450 if ($aSearchTerm['class'])
452 $aNewSearch['sClass'] = $aSearchTerm['class'];
453 $aNewSearch['sType'] = $aSearchTerm['type'];
454 $aNewSearches[] = $aNewSearch;
455 $bSpecialTerms = true;
459 $aSearches = $aNewSearches;
462 // Split query into phrases
463 // Commas are used to reduce the search space by indicating where phrases split
464 if ($this->aStructuredQuery)
466 $aPhrases = $aStructuredQuery;
467 $bStructuredPhrases = true;
471 $aPhrases = explode(',',$sQuery);
472 $bStructuredPhrases = false;
475 // Convert each phrase to standard form
476 // Create a list of standard words
477 // Get all 'sets' of words
478 // Generate a complete list of all
480 foreach($aPhrases as $iPhrase => $sPhrase)
482 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
483 if (PEAR::isError($aPhrase))
485 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
486 if (CONST_Debug) var_dump($aPhrase);
489 if (trim($aPhrase['string']))
491 $aPhrases[$iPhrase] = $aPhrase;
492 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
493 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
494 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
498 unset($aPhrases[$iPhrase]);
502 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
503 $aPhraseTypes = array_keys($aPhrases);
504 $aPhrases = array_values($aPhrases);
506 if (sizeof($aTokens))
508 // Check which tokens we have, get the ID numbers
509 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
510 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
512 if (CONST_Debug) var_Dump($sSQL);
514 $aValidTokens = array();
515 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
516 else $aDatabaseWords = array();
517 if (PEAR::IsError($aDatabaseWords))
519 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
521 $aPossibleMainWordIDs = array();
522 $aWordFrequencyScores = array();
523 foreach($aDatabaseWords as $aToken)
525 // Very special case - require 2 letter country param to match the country code found
526 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
527 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
532 if (isset($aValidTokens[$aToken['word_token']]))
534 $aValidTokens[$aToken['word_token']][] = $aToken;
538 $aValidTokens[$aToken['word_token']] = array($aToken);
540 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
541 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
543 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
545 // Try and calculate GB postcodes we might be missing
546 foreach($aTokens as $sToken)
548 // Source of gb postcodes is now definitive - always use
549 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
551 if (substr($aData[1],-2,1) != ' ')
553 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
554 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
556 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
557 if ($aGBPostcodeLocation)
559 $aValidTokens[$sToken] = $aGBPostcodeLocation;
562 // US ZIP+4 codes - if there is no token,
563 // merge in the 5-digit ZIP code
564 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
566 if (isset($aValidTokens[$aData[1]]))
568 foreach($aValidTokens[$aData[1]] as $aToken)
570 if (!$aToken['class'])
572 if (isset($aValidTokens[$sToken]))
574 $aValidTokens[$sToken][] = $aToken;
578 $aValidTokens[$sToken] = array($aToken);
586 foreach($aTokens as $sToken)
588 // Unknown single word token with a number - assume it is a house number
589 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
591 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
595 // Any words that have failed completely?
598 // Start the search process
599 $aResultPlaceIDs = array();
602 Calculate all searches using aValidTokens i.e.
603 'Wodsworth Road, Sheffield' =>
607 0 1 (wodsworth)(road)
610 Score how good the search is so they can be ordered
612 foreach($aPhrases as $iPhrase => $sPhrase)
614 $aNewPhraseSearches = array();
615 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
616 else $sPhraseType = '';
618 foreach($aPhrases[$iPhrase]['wordsets'] as $aWordset)
620 $aWordsetSearches = $aSearches;
622 // Add all words from this wordset
623 foreach($aWordset as $iToken => $sToken)
625 //echo "<br><b>$sToken</b>";
626 $aNewWordsetSearches = array();
628 foreach($aWordsetSearches as $aCurrentSearch)
631 //var_dump($aCurrentSearch);
634 // If the token is valid
635 if (isset($aValidTokens[' '.$sToken]))
637 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
639 $aSearch = $aCurrentSearch;
640 $aSearch['iSearchRank']++;
641 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
643 if ($aSearch['sCountryCode'] === false)
645 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
646 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
647 // If reverse order is enabled, it may appear at the beginning as well.
648 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
649 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
651 $aSearch['iSearchRank'] += 5;
653 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
656 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
658 if ($aSearch['fLat'] === '')
660 $aSearch['fLat'] = $aSearchTerm['lat'];
661 $aSearch['fLon'] = $aSearchTerm['lon'];
662 $aSearch['fRadius'] = $aSearchTerm['radius'];
663 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
666 elseif ($sPhraseType == 'postalcode')
668 // 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
669 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
671 // If we already have a name try putting the postcode first
672 if (sizeof($aSearch['aName']))
674 $aNewSearch = $aSearch;
675 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
676 $aNewSearch['aName'] = array();
677 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
678 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
681 if (sizeof($aSearch['aName']))
683 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
685 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
689 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
690 $aSearch['iSearchRank'] += 1000; // skip;
695 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
696 //$aSearch['iNamePhrase'] = $iPhrase;
698 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
702 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
704 if ($aSearch['sHouseNumber'] === '')
706 $aSearch['sHouseNumber'] = $sToken;
707 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
709 // Fall back to not searching for this item (better than nothing)
710 $aSearch = $aCurrentSearch;
711 $aSearch['iSearchRank'] += 1;
712 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
716 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
718 if ($aSearch['sClass'] === '')
720 $aSearch['sOperator'] = $aSearchTerm['operator'];
721 $aSearch['sClass'] = $aSearchTerm['class'];
722 $aSearch['sType'] = $aSearchTerm['type'];
723 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
724 else $aSearch['sOperator'] = 'near'; // near = in for the moment
726 // Do we have a shortcut id?
727 if ($aSearch['sOperator'] == 'name')
729 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
730 if ($iAmenityID = $this->oDB->getOne($sSQL))
732 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
733 $aSearch['aName'][$iAmenityID] = $iAmenityID;
734 $aSearch['sClass'] = '';
735 $aSearch['sType'] = '';
738 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
741 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
743 if (sizeof($aSearch['aName']))
745 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
747 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
751 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
752 $aSearch['iSearchRank'] += 1000; // skip;
757 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
758 //$aSearch['iNamePhrase'] = $iPhrase;
760 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
764 if (isset($aValidTokens[$sToken]))
766 // Allow searching for a word - but at extra cost
767 foreach($aValidTokens[$sToken] as $aSearchTerm)
769 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
771 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
773 $aSearch = $aCurrentSearch;
774 $aSearch['iSearchRank'] += 1;
775 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
777 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
778 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
780 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
782 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
784 if (empty($aSearchTermToken['country_code'])
785 && empty($aSearchTermToken['lat'])
786 && empty($aSearchTermToken['class']))
788 $aSearch = $aCurrentSearch;
789 $aSearch['iSearchRank'] += 1;
790 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
791 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
797 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
798 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
802 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
804 $aSearch = $aCurrentSearch;
805 $aSearch['iSearchRank'] += 2;
806 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
807 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
808 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
810 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
811 $aSearch['iNamePhrase'] = $iPhrase;
812 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
819 // Allow skipping a word - but at EXTREAM cost
820 //$aSearch = $aCurrentSearch;
821 //$aSearch['iSearchRank']+=100;
822 //$aNewWordsetSearches[] = $aSearch;
826 usort($aNewWordsetSearches, 'bySearchRank');
827 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
829 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
831 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
832 usort($aNewPhraseSearches, 'bySearchRank');
834 $aSearchHash = array();
835 foreach($aNewPhraseSearches as $iSearch => $aSearch)
837 $sHash = serialize($aSearch);
838 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
839 else $aSearchHash[$sHash] = 1;
842 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
845 // Re-group the searches by their score, junk anything over 20 as just not worth trying
846 $aGroupedSearches = array();
847 foreach($aNewPhraseSearches as $aSearch)
849 if ($aSearch['iSearchRank'] < $this->iMaxRank)
851 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
852 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
855 ksort($aGroupedSearches);
858 $aSearches = array();
859 foreach($aGroupedSearches as $iScore => $aNewSearches)
861 $iSearchCount += sizeof($aNewSearches);
862 $aSearches = array_merge($aSearches, $aNewSearches);
863 if ($iSearchCount > 50) break;
866 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
873 // Re-group the searches by their score, junk anything over 20 as just not worth trying
874 $aGroupedSearches = array();
875 foreach($aSearches as $aSearch)
877 if ($aSearch['iSearchRank'] < $this->iMaxRank)
879 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
880 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
883 ksort($aGroupedSearches);
886 if (CONST_Debug) var_Dump($aGroupedSearches);
888 if ($this->bReverseInPlan)
890 $aCopyGroupedSearches = $aGroupedSearches;
891 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
893 foreach($aSearches as $iSearch => $aSearch)
895 if (sizeof($aSearch['aAddress']))
897 $iReverseItem = array_pop($aSearch['aAddress']);
898 if (isset($aPossibleMainWordIDs[$iReverseItem]))
900 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
901 $aSearch['aName'] = array($iReverseItem);
902 $aGroupedSearches[$iGroup][] = $aSearch;
904 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
905 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
911 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
913 $aCopyGroupedSearches = $aGroupedSearches;
914 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
916 foreach($aSearches as $iSearch => $aSearch)
918 $aReductionsList = array($aSearch['aAddress']);
919 $iSearchRank = $aSearch['iSearchRank'];
920 while(sizeof($aReductionsList) > 0)
923 if ($iSearchRank > iMaxRank) break 3;
924 $aNewReductionsList = array();
925 foreach($aReductionsList as $aReductionsWordList)
927 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
929 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
930 $aReverseSearch = $aSearch;
931 $aSearch['aAddress'] = $aReductionsWordListResult;
932 $aSearch['iSearchRank'] = $iSearchRank;
933 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
934 if (sizeof($aReductionsWordListResult) > 0)
936 $aNewReductionsList[] = $aReductionsWordListResult;
940 $aReductionsList = $aNewReductionsList;
944 ksort($aGroupedSearches);
947 // Filter out duplicate searches
948 $aSearchHash = array();
949 foreach($aGroupedSearches as $iGroup => $aSearches)
951 foreach($aSearches as $iSearch => $aSearch)
953 $sHash = serialize($aSearch);
954 if (isset($aSearchHash[$sHash]))
956 unset($aGroupedSearches[$iGroup][$iSearch]);
957 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
961 $aSearchHash[$sHash] = 1;
966 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
970 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
973 foreach($aSearches as $aSearch)
977 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
978 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
981 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
983 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
985 // Just looking for a country by code - look it up
986 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
988 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
989 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
990 $sSQL .= " order by st_area(geometry) desc limit 1";
991 if (CONST_Debug) var_dump($sSQL);
992 $aPlaceIDs = $this->oDB->getCol($sSQL);
997 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
998 if (!$aSearch['sClass']) continue;
999 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1000 if ($this->oDB->getOne($sSQL))
1002 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1003 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1004 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
1005 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1006 if (sizeof($this->aExcludePlaceIDs))
1008 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1010 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1011 $sSQL .= " limit $this->iLimit";
1012 if (CONST_Debug) var_dump($sSQL);
1013 $aPlaceIDs = $this->oDB->getCol($sSQL);
1015 // If excluded place IDs are given, it is fair to assume that
1016 // there have been results in the small box, so no further
1017 // expansion in that case.
1018 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1020 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1021 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1022 $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
1023 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1024 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1025 $sSQL .= " limit $this->iLimit";
1026 if (CONST_Debug) var_dump($sSQL);
1027 $aPlaceIDs = $this->oDB->getCol($sSQL);
1032 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1033 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
1034 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1035 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1036 $sSQL .= " limit $this->iLimit";
1037 if (CONST_Debug) var_dump($sSQL);
1038 $aPlaceIDs = $this->oDB->getCol($sSQL);
1044 $aPlaceIDs = array();
1046 // First we need a position, either aName or fLat or both
1050 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1051 // they might be right - but they are just too darned expensive to run
1052 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1053 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1054 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1056 // For infrequent name terms disable index usage for address
1057 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1058 sizeof($aSearch['aName']) == 1 &&
1059 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1061 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1065 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1066 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1069 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1070 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1071 if ($aSearch['fLon'] && $aSearch['fLat'])
1073 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1074 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1076 if (sizeof($this->aExcludePlaceIDs))
1078 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1080 if ($sCountryCodesSQL)
1082 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1085 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
1086 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1088 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1089 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1090 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1091 $aOrder[] = "$sImportanceSQL DESC";
1092 if (sizeof($aSearch['aFullNameAddress']))
1094 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1097 if (sizeof($aTerms))
1099 $sSQL = "select place_id";
1100 $sSQL .= " from search_name";
1101 $sSQL .= " where ".join(' and ',$aTerms);
1102 $sSQL .= " order by ".join(', ',$aOrder);
1103 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1104 $sSQL .= " limit 50";
1105 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1106 $sSQL .= " limit 1";
1108 $sSQL .= " limit ".$this->iLimit;
1110 if (CONST_Debug) { var_dump($sSQL); }
1111 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1112 if (PEAR::IsError($aViewBoxPlaceIDs))
1114 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1116 //var_dump($aViewBoxPlaceIDs);
1117 // Did we have an viewbox matches?
1118 $aPlaceIDs = array();
1119 $bViewBoxMatch = false;
1120 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1122 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1123 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1124 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1125 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1126 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1129 //var_Dump($aPlaceIDs);
1132 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1134 $aRoadPlaceIDs = $aPlaceIDs;
1135 $sPlaceIDs = join(',',$aPlaceIDs);
1137 // Now they are indexed look for a house attached to a street we found
1138 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1139 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1140 if (sizeof($this->aExcludePlaceIDs))
1142 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1144 $sSQL .= " limit $this->iLimit";
1145 if (CONST_Debug) var_dump($sSQL);
1146 $aPlaceIDs = $this->oDB->getCol($sSQL);
1148 // If not try the aux fallback table
1149 if (!sizeof($aPlaceIDs))
1151 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1152 if (sizeof($this->aExcludePlaceIDs))
1154 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1156 //$sSQL .= " limit $this->iLimit";
1157 if (CONST_Debug) var_dump($sSQL);
1158 $aPlaceIDs = $this->oDB->getCol($sSQL);
1161 if (!sizeof($aPlaceIDs))
1163 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1164 if (sizeof($this->aExcludePlaceIDs))
1166 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1168 //$sSQL .= " limit $this->iLimit";
1169 if (CONST_Debug) var_dump($sSQL);
1170 $aPlaceIDs = $this->oDB->getCol($sSQL);
1173 // Fallback to the road
1174 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1176 $aPlaceIDs = $aRoadPlaceIDs;
1181 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1183 $sPlaceIDs = join(',',$aPlaceIDs);
1184 $aClassPlaceIDs = array();
1186 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1188 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1189 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1190 $sSQL .= " and linked_place_id is null";
1191 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1192 $sSQL .= " order by rank_search asc limit $this->iLimit";
1193 if (CONST_Debug) var_dump($sSQL);
1194 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1197 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1199 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1200 $bCacheTable = $this->oDB->getOne($sSQL);
1202 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1204 if (CONST_Debug) var_dump($sSQL);
1205 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1207 // For state / country level searches the normal radius search doesn't work very well
1208 $sPlaceGeom = false;
1209 if ($this->iMaxRank < 9 && $bCacheTable)
1211 // Try and get a polygon to search in instead
1212 $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";
1213 if (CONST_Debug) var_dump($sSQL);
1214 $sPlaceGeom = $this->oDB->getOne($sSQL);
1223 $this->iMaxRank += 5;
1224 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1225 if (CONST_Debug) var_dump($sSQL);
1226 $aPlaceIDs = $this->oDB->getCol($sSQL);
1227 $sPlaceIDs = join(',',$aPlaceIDs);
1230 if ($sPlaceIDs || $sPlaceGeom)
1236 // More efficient - can make the range bigger
1240 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1241 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1242 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1244 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1245 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1248 $sSQL .= ",placex as f where ";
1249 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1254 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1256 if (sizeof($this->aExcludePlaceIDs))
1258 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1260 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1261 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1262 if ($iOffset) $sSQL .= " offset $iOffset";
1263 $sSQL .= " limit $this->iLimit";
1264 if (CONST_Debug) var_dump($sSQL);
1265 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1269 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1272 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1273 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1275 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1276 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1277 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1278 if (sizeof($this->aExcludePlaceIDs))
1280 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1282 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1283 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1284 if ($iOffset) $sSQL .= " offset $iOffset";
1285 $sSQL .= " limit $this->iLimit";
1286 if (CONST_Debug) var_dump($sSQL);
1287 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1292 $aPlaceIDs = $aClassPlaceIDs;
1298 if (PEAR::IsError($aPlaceIDs))
1300 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1303 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1305 foreach($aPlaceIDs as $iPlaceID)
1307 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1309 if ($iQueryLoop > 20) break;
1312 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1314 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1315 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1316 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1317 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1318 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1319 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1320 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1321 if ($aAddressRankList) $sSQL .= " OR 30 in (".join(',',$aAddressRankList).")";
1323 if (CONST_Debug) var_dump($sSQL);
1324 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1328 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1329 if ($iGroupLoop > 4) break;
1330 if ($iQueryLoop > 30) break;
1333 // Did we find anything?
1334 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1336 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1342 // Just interpret as a reverse geocode
1343 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1344 $aSearchResults = $this->getDetails(array($iPlaceID));
1348 if (!sizeof($aSearchResults))
1353 $aClassType = getClassTypesWithImportance();
1354 $aRecheckWords = preg_split('/\b/u',$sQuery);
1355 foreach($aRecheckWords as $i => $sWord)
1357 if (!$sWord) unset($aRecheckWords[$i]);
1360 foreach($aSearchResults as $iResNum => $aResult)
1362 if (CONST_Search_AreaPolygons)
1364 // Get the bounding box and outline polygon
1365 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1366 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1367 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1368 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1369 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1370 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1371 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1372 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1373 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1374 $aPointPolygon = $this->oDB->getRow($sSQL);
1375 if (PEAR::IsError($aPointPolygon))
1377 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1380 if ($aPointPolygon['place_id'])
1382 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1383 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1384 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1385 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1387 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1389 $aResult['lat'] = $aPointPolygon['centrelat'];
1390 $aResult['lon'] = $aPointPolygon['centrelon'];
1392 if ($this->bIncludePolygonAsPoints)
1394 // Translate geometary string to point array
1395 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1397 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1399 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1401 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1403 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1406 $iSteps = ($fRadius * 40000)^2;
1407 $fStepSize = (2*pi())/$iSteps;
1408 $aPolyPoints = array();
1409 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1411 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1413 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1414 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1415 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1416 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1420 // Output data suitable for display (points and a bounding box)
1421 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1423 $aResult['aPolyPoints'] = array();
1424 foreach($aPolyPoints as $aPoint)
1426 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1429 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1433 if ($aResult['extra_place'] == 'city')
1435 $aResult['class'] = 'place';
1436 $aResult['type'] = 'city';
1437 $aResult['rank_search'] = 16;
1440 if (!isset($aResult['aBoundingBox']))
1443 $fDiameter = 0.0001;
1445 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1446 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1448 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1450 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1451 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1453 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1455 $fRadius = $fDiameter / 2;
1457 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1458 $fStepSize = (2*pi())/$iSteps;
1459 $aPolyPoints = array();
1460 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1462 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1464 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1465 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1466 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1467 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1469 // Output data suitable for display (points and a bounding box)
1470 if ($this->bIncludePolygonAsPoints)
1472 $aResult['aPolyPoints'] = array();
1473 foreach($aPolyPoints as $aPoint)
1475 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1478 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1481 // Is there an icon set for this type of result?
1482 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1483 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1485 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1488 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1489 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1491 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1494 if ($this->bIncludeAddressDetails)
1496 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1497 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1499 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1503 // Adjust importance for the number of exact string matches in the result
1504 $aResult['importance'] = max(0.001,$aResult['importance']);
1506 $sAddress = $aResult['langaddress'];
1507 foreach($aRecheckWords as $i => $sWord)
1509 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1512 $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
1514 $aResult['name'] = $aResult['langaddress'];
1515 $aResult['foundorder'] = -$aResult['addressimportance'];
1516 $aSearchResults[$iResNum] = $aResult;
1518 uasort($aSearchResults, 'byImportance');
1520 $aOSMIDDone = array();
1521 $aClassTypeNameDone = array();
1522 $aToFilter = $aSearchResults;
1523 $aSearchResults = array();
1526 foreach($aToFilter as $iResNum => $aResult)
1528 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1529 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1532 $fLat = $aResult['lat'];
1533 $fLon = $aResult['lon'];
1534 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1537 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1538 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1540 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1541 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1542 $aSearchResults[] = $aResult;
1545 // Absolute limit on number of results
1546 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1549 return $aSearchResults;
1558 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1560 $aPoints = explode(',',$_GET['route']);
1561 if (sizeof($aPoints) % 2 != 0)
1563 userError("Uneven number of points");
1566 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1567 $fPrevCoord = false;