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)
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 $this->iMinAddressRank and $this->iMaxAddressRank ";
238 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
239 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
241 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->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 (!$this->bDeDupe) $sSQL .= ",place_id";
245 $sSQL .= ",langaddress ";
246 $sSQL .= ",placename ";
248 $sSQL .= ",extratags->'place' ";
250 if (30 >= $this->iMinAddressRank && 30 <= $this->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 $this->iMinAddressRank and $this->iMaxAddressRank ";
263 $sSQL .= "group by place_id";
264 if (!$this->bDeDupe) $sSQL .= ",place_id ";
267 $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,";
268 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
269 $sSQL .= "null as placename,";
270 $sSQL .= "null as ref,";
271 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
272 $sSQL .= "-0.10 as importance, ";
273 $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, ";
274 $sSQL .= "null as extra_place ";
275 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
276 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
277 $sSQL .= "group by place_id";
278 if (!$this->bDeDupe) $sSQL .= ",place_id";
279 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
283 $sSQL .= " order by importance desc";
284 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
285 $aSearchResults = $this->oDB->getAll($sSQL);
287 if (PEAR::IsError($aSearchResults))
289 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
292 return $aSearchResults;
297 if (!$this->sQuery && !$this->aStructuredQuery) return false;
299 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
301 $sCountryCodesSQL = false;
302 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
304 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
307 // Hack to make it handle "new york, ny" (and variants) correctly
308 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
310 // Conflicts between US state abreviations and various words for 'the' in different languages
311 if (isset($this->aLangPrefOrder['name:en']))
313 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
314 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
315 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
319 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
320 $bBoundingBoxSearch = false;
323 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
324 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
325 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
326 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
327 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
328 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
330 $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)";
331 $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
332 $bBoundingBoxSearch = $this->bBoundedSearch;
336 if ($this->aRoutePoints)
338 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
340 foreach($this->aRouteaPoints as $aPoint)
342 if (!$bFirst) $sViewboxCentreSQL .= ",";
343 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
345 $sViewboxCentreSQL .= ")'::geometry,4326)";
347 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
348 $sViewboxSmallSQL = $this->oDB->getOne($sSQL);
349 if (PEAR::isError($sViewboxSmallSQL))
351 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
353 $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
355 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
356 $sViewboxLargeSQL = $this->oDB->getOne($sSQL);
357 if (PEAR::isError($sViewboxLargeSQL))
359 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
361 $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
362 $bBoundingBoxSearch = $this->bBoundedSearch;
365 // Do we have anything that looks like a lat/lon pair?
366 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
368 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
369 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
370 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
372 $this->setNearPoint(array($fQueryLat, $fQueryLon));
373 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
376 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
378 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
379 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
380 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
382 $this->setNearPoint(array($fQueryLat, $fQueryLon));
383 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
386 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
388 $fQueryLat = $aData[2];
389 $fQueryLon = $aData[3];
390 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
392 $this->setNearPoint(array($fQueryLat, $fQueryLon));
393 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
397 $aSearchResults = array();
398 if ($sQuery || $this->aStructuredQuery)
400 // Start with a blank search
402 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
403 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
404 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
407 // Do we have a radius search?
408 $sNearPointSQL = false;
409 if ($this->aNearPoint)
411 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
412 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
413 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
414 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
417 // Any 'special' terms in the search?
418 $bSpecialTerms = false;
419 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
420 $aSpecialTerms = array();
421 foreach($aSpecialTermsRaw as $aSpecialTerm)
423 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
424 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
427 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
428 $aSpecialTerms = array();
429 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
431 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
432 unset($aStructuredQuery['amenity']);
434 foreach($aSpecialTermsRaw as $aSpecialTerm)
436 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
437 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
438 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
439 $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';
440 if (CONST_Debug) var_Dump($sSQL);
441 $aSearchWords = $this->oDB->getAll($sSQL);
442 $aNewSearches = array();
443 foreach($aSearches as $aSearch)
445 foreach($aSearchWords as $aSearchTerm)
447 $aNewSearch = $aSearch;
448 if ($aSearchTerm['country_code'])
450 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
451 $aNewSearches[] = $aNewSearch;
452 $bSpecialTerms = true;
454 if ($aSearchTerm['class'])
456 $aNewSearch['sClass'] = $aSearchTerm['class'];
457 $aNewSearch['sType'] = $aSearchTerm['type'];
458 $aNewSearches[] = $aNewSearch;
459 $bSpecialTerms = true;
463 $aSearches = $aNewSearches;
466 // Split query into phrases
467 // Commas are used to reduce the search space by indicating where phrases split
468 if ($this->aStructuredQuery)
470 $aPhrases = $this->aStructuredQuery;
471 $bStructuredPhrases = true;
475 $aPhrases = explode(',',$sQuery);
476 $bStructuredPhrases = false;
479 // Convert each phrase to standard form
480 // Create a list of standard words
481 // Get all 'sets' of words
482 // Generate a complete list of all
484 foreach($aPhrases as $iPhrase => $sPhrase)
486 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
487 if (PEAR::isError($aPhrase))
489 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
490 if (CONST_Debug) var_dump($aPhrase);
493 if (trim($aPhrase['string']))
495 $aPhrases[$iPhrase] = $aPhrase;
496 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
497 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
498 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
502 unset($aPhrases[$iPhrase]);
506 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
507 $aPhraseTypes = array_keys($aPhrases);
508 $aPhrases = array_values($aPhrases);
510 if (sizeof($aTokens))
512 // Check which tokens we have, get the ID numbers
513 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
514 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
516 if (CONST_Debug) var_Dump($sSQL);
518 $aValidTokens = array();
519 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
520 else $aDatabaseWords = array();
521 if (PEAR::IsError($aDatabaseWords))
523 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
525 $aPossibleMainWordIDs = array();
526 $aWordFrequencyScores = array();
527 foreach($aDatabaseWords as $aToken)
529 // Very special case - require 2 letter country param to match the country code found
530 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
531 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
536 if (isset($aValidTokens[$aToken['word_token']]))
538 $aValidTokens[$aToken['word_token']][] = $aToken;
542 $aValidTokens[$aToken['word_token']] = array($aToken);
544 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
545 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
547 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
549 // Try and calculate GB postcodes we might be missing
550 foreach($aTokens as $sToken)
552 // Source of gb postcodes is now definitive - always use
553 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
555 if (substr($aData[1],-2,1) != ' ')
557 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
558 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
560 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
561 if ($aGBPostcodeLocation)
563 $aValidTokens[$sToken] = $aGBPostcodeLocation;
566 // US ZIP+4 codes - if there is no token,
567 // merge in the 5-digit ZIP code
568 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
570 if (isset($aValidTokens[$aData[1]]))
572 foreach($aValidTokens[$aData[1]] as $aToken)
574 if (!$aToken['class'])
576 if (isset($aValidTokens[$sToken]))
578 $aValidTokens[$sToken][] = $aToken;
582 $aValidTokens[$sToken] = array($aToken);
590 foreach($aTokens as $sToken)
592 // Unknown single word token with a number - assume it is a house number
593 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
595 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
599 // Any words that have failed completely?
602 // Start the search process
603 $aResultPlaceIDs = array();
606 Calculate all searches using aValidTokens i.e.
607 'Wodsworth Road, Sheffield' =>
611 0 1 (wodsworth)(road)
614 Score how good the search is so they can be ordered
616 foreach($aPhrases as $iPhrase => $sPhrase)
618 $aNewPhraseSearches = array();
619 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
620 else $sPhraseType = '';
622 foreach($aPhrases[$iPhrase]['wordsets'] as $aWordset)
624 $aWordsetSearches = $aSearches;
626 // Add all words from this wordset
627 foreach($aWordset as $iToken => $sToken)
629 //echo "<br><b>$sToken</b>";
630 $aNewWordsetSearches = array();
632 foreach($aWordsetSearches as $aCurrentSearch)
635 //var_dump($aCurrentSearch);
638 // If the token is valid
639 if (isset($aValidTokens[' '.$sToken]))
641 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
643 $aSearch = $aCurrentSearch;
644 $aSearch['iSearchRank']++;
645 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
647 if ($aSearch['sCountryCode'] === false)
649 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
650 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
651 // If reverse order is enabled, it may appear at the beginning as well.
652 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
653 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
655 $aSearch['iSearchRank'] += 5;
657 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
660 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
662 if ($aSearch['fLat'] === '')
664 $aSearch['fLat'] = $aSearchTerm['lat'];
665 $aSearch['fLon'] = $aSearchTerm['lon'];
666 $aSearch['fRadius'] = $aSearchTerm['radius'];
667 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
670 elseif ($sPhraseType == 'postalcode')
672 // 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
673 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
675 // If we already have a name try putting the postcode first
676 if (sizeof($aSearch['aName']))
678 $aNewSearch = $aSearch;
679 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
680 $aNewSearch['aName'] = array();
681 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
682 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
685 if (sizeof($aSearch['aName']))
687 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
689 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
693 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
694 $aSearch['iSearchRank'] += 1000; // skip;
699 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
700 //$aSearch['iNamePhrase'] = $iPhrase;
702 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
706 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
708 if ($aSearch['sHouseNumber'] === '')
710 $aSearch['sHouseNumber'] = $sToken;
711 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
713 // Fall back to not searching for this item (better than nothing)
714 $aSearch = $aCurrentSearch;
715 $aSearch['iSearchRank'] += 1;
716 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
720 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
722 if ($aSearch['sClass'] === '')
724 $aSearch['sOperator'] = $aSearchTerm['operator'];
725 $aSearch['sClass'] = $aSearchTerm['class'];
726 $aSearch['sType'] = $aSearchTerm['type'];
727 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
728 else $aSearch['sOperator'] = 'near'; // near = in for the moment
729 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
731 // Do we have a shortcut id?
732 if ($aSearch['sOperator'] == 'name')
734 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
735 if ($iAmenityID = $this->oDB->getOne($sSQL))
737 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
738 $aSearch['aName'][$iAmenityID] = $iAmenityID;
739 $aSearch['sClass'] = '';
740 $aSearch['sType'] = '';
743 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
746 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
748 if (sizeof($aSearch['aName']))
750 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
752 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
756 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
757 $aSearch['iSearchRank'] += 1000; // skip;
762 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
763 //$aSearch['iNamePhrase'] = $iPhrase;
765 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
769 if (isset($aValidTokens[$sToken]))
771 // Allow searching for a word - but at extra cost
772 foreach($aValidTokens[$sToken] as $aSearchTerm)
774 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
776 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
778 $aSearch = $aCurrentSearch;
779 $aSearch['iSearchRank'] += 1;
780 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
782 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
783 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
785 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
787 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
789 if (empty($aSearchTermToken['country_code'])
790 && empty($aSearchTermToken['lat'])
791 && empty($aSearchTermToken['class']))
793 $aSearch = $aCurrentSearch;
794 $aSearch['iSearchRank'] += 1;
795 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
796 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
802 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
803 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
807 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
809 $aSearch = $aCurrentSearch;
810 $aSearch['iSearchRank'] += 2;
811 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
812 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
813 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
815 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
816 $aSearch['iNamePhrase'] = $iPhrase;
817 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
824 // Allow skipping a word - but at EXTREAM cost
825 //$aSearch = $aCurrentSearch;
826 //$aSearch['iSearchRank']+=100;
827 //$aNewWordsetSearches[] = $aSearch;
831 usort($aNewWordsetSearches, 'bySearchRank');
832 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
834 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
836 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
837 usort($aNewPhraseSearches, 'bySearchRank');
839 $aSearchHash = array();
840 foreach($aNewPhraseSearches as $iSearch => $aSearch)
842 $sHash = serialize($aSearch);
843 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
844 else $aSearchHash[$sHash] = 1;
847 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
850 // Re-group the searches by their score, junk anything over 20 as just not worth trying
851 $aGroupedSearches = array();
852 foreach($aNewPhraseSearches as $aSearch)
854 if ($aSearch['iSearchRank'] < $this->iMaxRank)
856 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
857 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
860 ksort($aGroupedSearches);
863 $aSearches = array();
864 foreach($aGroupedSearches as $iScore => $aNewSearches)
866 $iSearchCount += sizeof($aNewSearches);
867 $aSearches = array_merge($aSearches, $aNewSearches);
868 if ($iSearchCount > 50) break;
871 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
878 // Re-group the searches by their score, junk anything over 20 as just not worth trying
879 $aGroupedSearches = array();
880 foreach($aSearches as $aSearch)
882 if ($aSearch['iSearchRank'] < $this->iMaxRank)
884 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
885 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
888 ksort($aGroupedSearches);
891 if (CONST_Debug) var_Dump($aGroupedSearches);
893 if ($this->bReverseInPlan)
895 $aCopyGroupedSearches = $aGroupedSearches;
896 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
898 foreach($aSearches as $iSearch => $aSearch)
900 if (sizeof($aSearch['aAddress']))
902 $iReverseItem = array_pop($aSearch['aAddress']);
903 if (isset($aPossibleMainWordIDs[$iReverseItem]))
905 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
906 $aSearch['aName'] = array($iReverseItem);
907 $aGroupedSearches[$iGroup][] = $aSearch;
909 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
910 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
916 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
918 $aCopyGroupedSearches = $aGroupedSearches;
919 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
921 foreach($aSearches as $iSearch => $aSearch)
923 $aReductionsList = array($aSearch['aAddress']);
924 $iSearchRank = $aSearch['iSearchRank'];
925 while(sizeof($aReductionsList) > 0)
928 if ($iSearchRank > iMaxRank) break 3;
929 $aNewReductionsList = array();
930 foreach($aReductionsList as $aReductionsWordList)
932 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
934 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
935 $aReverseSearch = $aSearch;
936 $aSearch['aAddress'] = $aReductionsWordListResult;
937 $aSearch['iSearchRank'] = $iSearchRank;
938 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
939 if (sizeof($aReductionsWordListResult) > 0)
941 $aNewReductionsList[] = $aReductionsWordListResult;
945 $aReductionsList = $aNewReductionsList;
949 ksort($aGroupedSearches);
952 // Filter out duplicate searches
953 $aSearchHash = array();
954 foreach($aGroupedSearches as $iGroup => $aSearches)
956 foreach($aSearches as $iSearch => $aSearch)
958 $sHash = serialize($aSearch);
959 if (isset($aSearchHash[$sHash]))
961 unset($aGroupedSearches[$iGroup][$iSearch]);
962 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
966 $aSearchHash[$sHash] = 1;
971 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
975 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
978 foreach($aSearches as $aSearch)
982 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
983 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
986 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
988 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
990 // Just looking for a country by code - look it up
991 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
993 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
994 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
995 $sSQL .= " order by st_area(geometry) desc limit 1";
996 if (CONST_Debug) var_dump($sSQL);
997 $aPlaceIDs = $this->oDB->getCol($sSQL);
1002 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1003 if (!$aSearch['sClass']) continue;
1004 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1005 if ($this->oDB->getOne($sSQL))
1007 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1008 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1009 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
1010 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1011 if (sizeof($this->aExcludePlaceIDs))
1013 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1015 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1016 $sSQL .= " limit $this->iLimit";
1017 if (CONST_Debug) var_dump($sSQL);
1018 $aPlaceIDs = $this->oDB->getCol($sSQL);
1020 // If excluded place IDs are given, it is fair to assume that
1021 // there have been results in the small box, so no further
1022 // expansion in that case.
1023 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1025 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1026 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1027 $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
1028 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1029 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1030 $sSQL .= " limit $this->iLimit";
1031 if (CONST_Debug) var_dump($sSQL);
1032 $aPlaceIDs = $this->oDB->getCol($sSQL);
1037 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1038 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
1039 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1040 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1041 $sSQL .= " limit $this->iLimit";
1042 if (CONST_Debug) var_dump($sSQL);
1043 $aPlaceIDs = $this->oDB->getCol($sSQL);
1049 $aPlaceIDs = array();
1051 // First we need a position, either aName or fLat or both
1055 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1056 // they might be right - but they are just too darned expensive to run
1057 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1058 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1059 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1061 // For infrequent name terms disable index usage for address
1062 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1063 sizeof($aSearch['aName']) == 1 &&
1064 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1066 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1070 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1071 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1074 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1075 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1076 if ($aSearch['fLon'] && $aSearch['fLat'])
1078 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1079 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1081 if (sizeof($this->aExcludePlaceIDs))
1083 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1085 if ($sCountryCodesSQL)
1087 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1090 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
1091 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1093 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1094 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1095 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1096 $aOrder[] = "$sImportanceSQL DESC";
1097 if (sizeof($aSearch['aFullNameAddress']))
1099 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1102 if (sizeof($aTerms))
1104 $sSQL = "select place_id";
1105 $sSQL .= " from search_name";
1106 $sSQL .= " where ".join(' and ',$aTerms);
1107 $sSQL .= " order by ".join(', ',$aOrder);
1108 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1109 $sSQL .= " limit 50";
1110 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1111 $sSQL .= " limit 1";
1113 $sSQL .= " limit ".$this->iLimit;
1115 if (CONST_Debug) { var_dump($sSQL); }
1116 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1117 if (PEAR::IsError($aViewBoxPlaceIDs))
1119 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1121 //var_dump($aViewBoxPlaceIDs);
1122 // Did we have an viewbox matches?
1123 $aPlaceIDs = array();
1124 $bViewBoxMatch = false;
1125 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1127 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1128 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1129 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1130 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1131 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1134 //var_Dump($aPlaceIDs);
1137 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1139 $aRoadPlaceIDs = $aPlaceIDs;
1140 $sPlaceIDs = join(',',$aPlaceIDs);
1142 // Now they are indexed look for a house attached to a street we found
1143 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1144 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1145 if (sizeof($this->aExcludePlaceIDs))
1147 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1149 $sSQL .= " limit $this->iLimit";
1150 if (CONST_Debug) var_dump($sSQL);
1151 $aPlaceIDs = $this->oDB->getCol($sSQL);
1153 // If not try the aux fallback table
1155 if (!sizeof($aPlaceIDs))
1157 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1158 if (sizeof($this->aExcludePlaceIDs))
1160 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1162 //$sSQL .= " limit $this->iLimit";
1163 if (CONST_Debug) var_dump($sSQL);
1164 $aPlaceIDs = $this->oDB->getCol($sSQL);
1168 if (!sizeof($aPlaceIDs))
1170 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1171 if (sizeof($this->aExcludePlaceIDs))
1173 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1175 //$sSQL .= " limit $this->iLimit";
1176 if (CONST_Debug) var_dump($sSQL);
1177 $aPlaceIDs = $this->oDB->getCol($sSQL);
1180 // Fallback to the road
1181 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1183 $aPlaceIDs = $aRoadPlaceIDs;
1188 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1190 $sPlaceIDs = join(',',$aPlaceIDs);
1191 $aClassPlaceIDs = array();
1193 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1195 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1196 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1197 $sSQL .= " and linked_place_id is null";
1198 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1199 $sSQL .= " order by rank_search asc limit $this->iLimit";
1200 if (CONST_Debug) var_dump($sSQL);
1201 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1204 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1206 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1207 $bCacheTable = $this->oDB->getOne($sSQL);
1209 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1211 if (CONST_Debug) var_dump($sSQL);
1212 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1214 // For state / country level searches the normal radius search doesn't work very well
1215 $sPlaceGeom = false;
1216 if ($this->iMaxRank < 9 && $bCacheTable)
1218 // Try and get a polygon to search in instead
1219 $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";
1220 if (CONST_Debug) var_dump($sSQL);
1221 $sPlaceGeom = $this->oDB->getOne($sSQL);
1230 $this->iMaxRank += 5;
1231 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1232 if (CONST_Debug) var_dump($sSQL);
1233 $aPlaceIDs = $this->oDB->getCol($sSQL);
1234 $sPlaceIDs = join(',',$aPlaceIDs);
1237 if ($sPlaceIDs || $sPlaceGeom)
1243 // More efficient - can make the range bigger
1247 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1248 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1249 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1251 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1252 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1255 $sSQL .= ",placex as f where ";
1256 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1261 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1263 if (sizeof($this->aExcludePlaceIDs))
1265 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1267 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1268 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1269 if ($iOffset) $sSQL .= " offset $iOffset";
1270 $sSQL .= " limit $this->iLimit";
1271 if (CONST_Debug) var_dump($sSQL);
1272 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1276 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1279 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1280 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1282 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1283 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1284 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1285 if (sizeof($this->aExcludePlaceIDs))
1287 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1289 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1290 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1291 if ($iOffset) $sSQL .= " offset $iOffset";
1292 $sSQL .= " limit $this->iLimit";
1293 if (CONST_Debug) var_dump($sSQL);
1294 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1299 $aPlaceIDs = $aClassPlaceIDs;
1305 if (PEAR::IsError($aPlaceIDs))
1307 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1310 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1312 foreach($aPlaceIDs as $iPlaceID)
1314 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1316 if ($iQueryLoop > 20) break;
1319 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1321 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1322 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1323 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1324 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1325 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1326 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1327 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1328 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1330 if (CONST_Debug) var_dump($sSQL);
1331 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1335 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1336 if ($iGroupLoop > 4) break;
1337 if ($iQueryLoop > 30) break;
1340 // Did we find anything?
1341 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1343 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1349 // Just interpret as a reverse geocode
1350 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1352 $aSearchResults = $this->getDetails(array($iPlaceID));
1354 $aSearchResults = array();
1358 if (!sizeof($aSearchResults))
1363 $aClassType = getClassTypesWithImportance();
1364 $aRecheckWords = preg_split('/\b/u',$sQuery);
1365 foreach($aRecheckWords as $i => $sWord)
1367 if (!$sWord) unset($aRecheckWords[$i]);
1370 foreach($aSearchResults as $iResNum => $aResult)
1372 if (CONST_Search_AreaPolygons)
1374 // Get the bounding box and outline polygon
1375 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1376 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1377 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1378 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1379 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1380 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1381 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1382 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1383 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1384 $aPointPolygon = $this->oDB->getRow($sSQL);
1385 if (PEAR::IsError($aPointPolygon))
1387 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1390 if ($aPointPolygon['place_id'])
1392 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1393 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1394 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1395 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1397 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1399 $aResult['lat'] = $aPointPolygon['centrelat'];
1400 $aResult['lon'] = $aPointPolygon['centrelon'];
1403 if ($this->bIncludePolygonAsPoints)
1405 // Translate geometary string to point array
1406 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1408 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1410 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1412 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1414 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1417 $iSteps = ($fRadius * 40000)^2;
1418 $fStepSize = (2*pi())/$iSteps;
1419 $aPolyPoints = array();
1420 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1422 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1424 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1425 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1426 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1427 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1431 // Output data suitable for display (points and a bounding box)
1432 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1434 $aResult['aPolyPoints'] = array();
1435 foreach($aPolyPoints as $aPoint)
1437 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1440 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1444 if ($aResult['extra_place'] == 'city')
1446 $aResult['class'] = 'place';
1447 $aResult['type'] = 'city';
1448 $aResult['rank_search'] = 16;
1451 if (!isset($aResult['aBoundingBox']))
1454 $fDiameter = 0.0001;
1456 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1457 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1459 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1461 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1462 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1464 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1466 $fRadius = $fDiameter / 2;
1468 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1469 $fStepSize = (2*pi())/$iSteps;
1470 $aPolyPoints = array();
1471 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1473 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1475 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1476 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1477 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1478 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1480 // Output data suitable for display (points and a bounding box)
1481 if ($this->bIncludePolygonAsPoints)
1483 $aResult['aPolyPoints'] = array();
1484 foreach($aPolyPoints as $aPoint)
1486 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1489 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1492 // Is there an icon set for this type of result?
1493 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1494 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1496 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1499 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1500 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1502 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1504 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1505 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1507 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1510 if ($this->bIncludeAddressDetails)
1512 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1513 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1515 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1519 // Adjust importance for the number of exact string matches in the result
1520 $aResult['importance'] = max(0.001,$aResult['importance']);
1522 $sAddress = $aResult['langaddress'];
1523 foreach($aRecheckWords as $i => $sWord)
1525 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1528 $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
1530 $aResult['name'] = $aResult['langaddress'];
1531 $aResult['foundorder'] = -$aResult['addressimportance'];
1532 $aSearchResults[$iResNum] = $aResult;
1534 uasort($aSearchResults, 'byImportance');
1536 $aOSMIDDone = array();
1537 $aClassTypeNameDone = array();
1538 $aToFilter = $aSearchResults;
1539 $aSearchResults = array();
1542 foreach($aToFilter as $iResNum => $aResult)
1544 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1545 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1548 $fLat = $aResult['lat'];
1549 $fLon = $aResult['lon'];
1550 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1553 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1554 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1556 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1557 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1558 $aSearchResults[] = $aResult;
1561 // Absolute limit on number of results
1562 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1565 return $aSearchResults;
1574 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1576 $aPoints = explode(',',$_GET['route']);
1577 if (sizeof($aPoints) % 2 != 0)
1579 userError("Uneven number of points");
1582 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1583 $fPrevCoord = false;