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 setReverseInPlan($bReverse)
48 $this->bReverseInPlan = $bReverse;
51 function setLanguagePreference($aLangPref)
53 $this->aLangPrefOrder = $aLangPref;
56 function setIncludeAddressDetails($bAddressDetails = true)
58 $this->bIncludeAddressDetails = (bool)$bAddressDetails;
61 function getIncludeAddressDetails()
63 return $this->bIncludeAddressDetails;
66 function setIncludePolygonAsPoints($b = true)
68 $this->bIncludePolygonAsPoints = $b;
71 function getIncludePolygonAsPoints()
73 return $this->bIncludePolygonAsPoints;
76 function setIncludePolygonAsText($b = true)
78 $this->bIncludePolygonAsText = $b;
81 function getIncludePolygonAsText()
83 return $this->bIncludePolygonAsText;
86 function setIncludePolygonAsGeoJSON($b = true)
88 $this->bIncludePolygonAsGeoJSON = $b;
91 function setIncludePolygonAsKML($b = true)
93 $this->bIncludePolygonAsKML = $b;
96 function setIncludePolygonAsSVG($b = true)
98 $this->bIncludePolygonAsSVG = $b;
101 function setDeDupe($bDeDupe = true)
103 $this->bDeDupe = (bool)$bDeDupe;
106 function setLimit($iLimit = 10)
108 if ($iLimit > 50) $iLimit = 50;
109 if ($iLimit < 1) $iLimit = 1;
111 $this->iFinalLimit = $iLimit;
112 $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
115 function setOffset($iOffset = 0)
117 $this->iOffset = $iOffset;
120 function setExcludedPlaceIDs($a)
122 // TODO: force to int
123 $this->aExcludePlaceIDs = $a;
126 function getExcludedPlaceIDs()
128 return $this->aExcludePlaceIDs;
131 function setBounded($bBoundedSearch = true)
133 $this->bBoundedSearch = (bool)$bBoundedSearch;
136 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
138 $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
141 function getViewBoxString()
143 if (!$this->aViewBox) return null;
144 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
147 function setRoute($aRoutePoints)
149 $this->aRoutePoints = $aRoutePoints;
152 function setFeatureType($sFeatureType)
154 switch($sFeatureType)
157 $this->setRankRange(4, 4);
160 $this->setRankRange(8, 8);
163 $this->setRankRange(14, 16);
166 $this->setRankRange(8, 20);
171 function setRankRange($iMin, $iMax)
173 $this->iMinAddressRank = (int)$iMin;
174 $this->iMaxAddressRank = (int)$iMax;
177 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
179 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
182 function setCountryCodesList($aCountryCodes)
184 $this->aCountryCodes = $aCountryCodes;
187 function setQuery($sQueryString)
189 $this->sQuery = $sQueryString;
190 $this->aStructuredQuery = false;
193 function getQueryString()
195 return $this->sQuery;
198 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
200 $sValue = trim($sValue);
201 if (!$sValue) return false;
202 $this->aStructuredQuery[$sKey] = $sValue;
203 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
205 $this->iMinAddressRank = $iNewMinAddressRank;
206 $this->iMaxAddressRank = $iNewMaxAddressRank;
208 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
212 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
214 $this->sQuery = false;
216 $this->aStructuredQuery = array();
217 $this->sAllowedTypesSQLList = '';
219 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
220 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
221 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
222 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
223 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
224 $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
225 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
227 if (sizeof($this->aStructuredQuery) > 0)
229 $this->sQuery = join(', ', $this->aStructuredQuery);
230 if ($this->iMaxAddressRank < 30)
232 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
238 function getDetails($aPlaceIDs)
240 if (sizeof($aPlaceIDs) == 0) return array();
242 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
244 // Get the details for display (is this a redundant extra step?)
245 $sPlaceIDs = join(',',$aPlaceIDs);
247 $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,";
248 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
249 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
250 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
251 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
252 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
253 $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, ";
254 $sSQL .= "(extratags->'place') as extra_place ";
255 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
256 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
257 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
258 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
260 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
261 $sSQL .= "and linked_place_id is null ";
262 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
263 if (!$this->bDeDupe) $sSQL .= ",place_id";
264 $sSQL .= ",langaddress ";
265 $sSQL .= ",placename ";
267 $sSQL .= ",extratags->'place' ";
269 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
272 $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,";
273 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
274 $sSQL .= "null as placename,";
275 $sSQL .= "null as ref,";
276 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
277 $sSQL .= "-0.15 as importance, ";
278 $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, ";
279 $sSQL .= "null as extra_place ";
280 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
281 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
282 $sSQL .= "group by place_id";
283 if (!$this->bDeDupe) $sSQL .= ",place_id";
285 $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,";
286 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
287 $sSQL .= "null as placename,";
288 $sSQL .= "null as ref,";
289 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
290 $sSQL .= "-0.10 as importance, ";
291 $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, ";
292 $sSQL .= "null as extra_place ";
293 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
294 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
295 $sSQL .= "group by place_id";
296 if (!$this->bDeDupe) $sSQL .= ",place_id";
297 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
300 $sSQL .= "order by importance desc";
301 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
302 $aSearchResults = $this->oDB->getAll($sSQL);
304 if (PEAR::IsError($aSearchResults))
306 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
309 return $aSearchResults;
312 /* Perform the actual query lookup.
314 Returns an ordered list of results, each with the following fields:
315 osm_type: type of corresponding OSM object
319 P - postcode (internally computed)
320 osm_id: id of corresponding OSM object
321 class: general object class (corresponds to tag key of primary OSM tag)
322 type: subclass of object (corresponds to tag value of primary OSM tag)
323 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
324 rank_search: rank in search hierarchy
325 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
326 rank_address: rank in address hierarchy (determines orer in address)
327 place_id: internal key (may differ between different instances)
328 country_code: ISO country code
329 langaddress: localized full address
330 placename: localized name of object
331 ref: content of ref tag (if available)
334 importance: importance of place based on Wikipedia link count
335 addressimportance: cumulated importance of address elements
336 extra_place: type of place (for admin boundaries, if there is a place tag)
337 aBoundingBox: bounding Box
338 label: short description of the object class/type (English only)
339 name: full name (currently the same as langaddress)
340 foundorder: further ordering value for places with same importance
344 if (!$this->sQuery && !$this->aStructuredQuery) return false;
346 $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
348 $sCountryCodesSQL = false;
349 if ($this->aCountryCodes && sizeof($this->aCountryCodes))
351 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
354 // Hack to make it handle "new york, ny" (and variants) correctly
355 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
357 // Conflicts between US state abreviations and various words for 'the' in different languages
358 if (isset($this->aLangPrefOrder['name:en']))
360 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
361 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
362 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
366 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
367 $bBoundingBoxSearch = false;
370 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
371 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
372 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
373 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
374 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
375 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
377 $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)";
378 $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
379 $bBoundingBoxSearch = $this->bBoundedSearch;
383 if ($this->aRoutePoints)
385 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
387 foreach($this->aRouteaPoints as $aPoint)
389 if (!$bFirst) $sViewboxCentreSQL .= ",";
390 $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
392 $sViewboxCentreSQL .= ")'::geometry,4326)";
394 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
395 $sViewboxSmallSQL = $this->oDB->getOne($sSQL);
396 if (PEAR::isError($sViewboxSmallSQL))
398 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
400 $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
402 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
403 $sViewboxLargeSQL = $this->oDB->getOne($sSQL);
404 if (PEAR::isError($sViewboxLargeSQL))
406 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
408 $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
409 $bBoundingBoxSearch = $this->bBoundedSearch;
412 // Do we have anything that looks like a lat/lon pair?
413 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
415 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
416 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
417 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
419 $this->setNearPoint(array($fQueryLat, $fQueryLon));
420 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
423 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
425 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
426 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
427 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
429 $this->setNearPoint(array($fQueryLat, $fQueryLon));
430 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
433 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
435 $fQueryLat = $aData[2];
436 $fQueryLon = $aData[3];
437 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
439 $this->setNearPoint(array($fQueryLat, $fQueryLon));
440 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
444 $aSearchResults = array();
445 if ($sQuery || $this->aStructuredQuery)
447 // Start with a blank search
449 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
450 'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
451 'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
454 // Do we have a radius search?
455 $sNearPointSQL = false;
456 if ($this->aNearPoint)
458 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
459 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
460 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
461 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
464 // Any 'special' terms in the search?
465 $bSpecialTerms = false;
466 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
467 $aSpecialTerms = array();
468 foreach($aSpecialTermsRaw as $aSpecialTerm)
470 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
471 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
474 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
475 $aSpecialTerms = array();
476 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
478 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
479 unset($aStructuredQuery['amenity']);
481 foreach($aSpecialTermsRaw as $aSpecialTerm)
483 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
484 $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
485 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
486 $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';
487 if (CONST_Debug) var_Dump($sSQL);
488 $aSearchWords = $this->oDB->getAll($sSQL);
489 $aNewSearches = array();
490 foreach($aSearches as $aSearch)
492 foreach($aSearchWords as $aSearchTerm)
494 $aNewSearch = $aSearch;
495 if ($aSearchTerm['country_code'])
497 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
498 $aNewSearches[] = $aNewSearch;
499 $bSpecialTerms = true;
501 if ($aSearchTerm['class'])
503 $aNewSearch['sClass'] = $aSearchTerm['class'];
504 $aNewSearch['sType'] = $aSearchTerm['type'];
505 $aNewSearches[] = $aNewSearch;
506 $bSpecialTerms = true;
510 $aSearches = $aNewSearches;
513 // Split query into phrases
514 // Commas are used to reduce the search space by indicating where phrases split
515 if ($this->aStructuredQuery)
517 $aPhrases = $this->aStructuredQuery;
518 $bStructuredPhrases = true;
522 $aPhrases = explode(',',$sQuery);
523 $bStructuredPhrases = false;
526 // Convert each phrase to standard form
527 // Create a list of standard words
528 // Get all 'sets' of words
529 // Generate a complete list of all
531 foreach($aPhrases as $iPhrase => $sPhrase)
533 $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
534 if (PEAR::isError($aPhrase))
536 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
537 if (CONST_Debug) var_dump($aPhrase);
540 if (trim($aPhrase['string']))
542 $aPhrases[$iPhrase] = $aPhrase;
543 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
544 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
545 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
549 unset($aPhrases[$iPhrase]);
553 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
554 $aPhraseTypes = array_keys($aPhrases);
555 $aPhrases = array_values($aPhrases);
557 if (sizeof($aTokens))
559 // Check which tokens we have, get the ID numbers
560 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
561 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
563 if (CONST_Debug) var_Dump($sSQL);
565 $aValidTokens = array();
566 if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
567 else $aDatabaseWords = array();
568 if (PEAR::IsError($aDatabaseWords))
570 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
572 $aPossibleMainWordIDs = array();
573 $aWordFrequencyScores = array();
574 foreach($aDatabaseWords as $aToken)
576 // Very special case - require 2 letter country param to match the country code found
577 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
578 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
583 if (isset($aValidTokens[$aToken['word_token']]))
585 $aValidTokens[$aToken['word_token']][] = $aToken;
589 $aValidTokens[$aToken['word_token']] = array($aToken);
591 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
592 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
594 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
596 // Try and calculate GB postcodes we might be missing
597 foreach($aTokens as $sToken)
599 // Source of gb postcodes is now definitive - always use
600 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
602 if (substr($aData[1],-2,1) != ' ')
604 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
605 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
607 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
608 if ($aGBPostcodeLocation)
610 $aValidTokens[$sToken] = $aGBPostcodeLocation;
613 // US ZIP+4 codes - if there is no token,
614 // merge in the 5-digit ZIP code
615 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
617 if (isset($aValidTokens[$aData[1]]))
619 foreach($aValidTokens[$aData[1]] as $aToken)
621 if (!$aToken['class'])
623 if (isset($aValidTokens[$sToken]))
625 $aValidTokens[$sToken][] = $aToken;
629 $aValidTokens[$sToken] = array($aToken);
637 foreach($aTokens as $sToken)
639 // Unknown single word token with a number - assume it is a house number
640 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
642 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
646 // Any words that have failed completely?
649 // Start the search process
650 $aResultPlaceIDs = array();
653 Calculate all searches using aValidTokens i.e.
654 'Wodsworth Road, Sheffield' =>
658 0 1 (wodsworth)(road)
661 Score how good the search is so they can be ordered
663 foreach($aPhrases as $iPhrase => $sPhrase)
665 $aNewPhraseSearches = array();
666 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
667 else $sPhraseType = '';
669 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
671 // Too many permutations - too expensive
672 if ($iWordSet > 120) break;
674 $aWordsetSearches = $aSearches;
676 // Add all words from this wordset
677 foreach($aWordset as $iToken => $sToken)
679 //echo "<br><b>$sToken</b>";
680 $aNewWordsetSearches = array();
682 foreach($aWordsetSearches as $aCurrentSearch)
685 //var_dump($aCurrentSearch);
688 // If the token is valid
689 if (isset($aValidTokens[' '.$sToken]))
691 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
693 $aSearch = $aCurrentSearch;
694 $aSearch['iSearchRank']++;
695 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
697 if ($aSearch['sCountryCode'] === false)
699 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
700 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
701 // If reverse order is enabled, it may appear at the beginning as well.
702 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
703 (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
705 $aSearch['iSearchRank'] += 5;
707 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
710 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
712 if ($aSearch['fLat'] === '')
714 $aSearch['fLat'] = $aSearchTerm['lat'];
715 $aSearch['fLon'] = $aSearchTerm['lon'];
716 $aSearch['fRadius'] = $aSearchTerm['radius'];
717 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
720 elseif ($sPhraseType == 'postalcode')
722 // 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
723 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
725 // If we already have a name try putting the postcode first
726 if (sizeof($aSearch['aName']))
728 $aNewSearch = $aSearch;
729 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
730 $aNewSearch['aName'] = array();
731 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
732 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
735 if (sizeof($aSearch['aName']))
737 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
739 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
743 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
744 $aSearch['iSearchRank'] += 1000; // skip;
749 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
750 //$aSearch['iNamePhrase'] = $iPhrase;
752 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
756 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
758 if ($aSearch['sHouseNumber'] === '')
760 $aSearch['sHouseNumber'] = $sToken;
761 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
763 // Fall back to not searching for this item (better than nothing)
764 $aSearch = $aCurrentSearch;
765 $aSearch['iSearchRank'] += 1;
766 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
770 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
772 if ($aSearch['sClass'] === '')
774 $aSearch['sOperator'] = $aSearchTerm['operator'];
775 $aSearch['sClass'] = $aSearchTerm['class'];
776 $aSearch['sType'] = $aSearchTerm['type'];
777 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
778 else $aSearch['sOperator'] = 'near'; // near = in for the moment
780 // Do we have a shortcut id?
781 if ($aSearch['sOperator'] == 'name')
783 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
784 if ($iAmenityID = $this->oDB->getOne($sSQL))
786 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
787 $aSearch['aName'][$iAmenityID] = $iAmenityID;
788 $aSearch['sClass'] = '';
789 $aSearch['sType'] = '';
792 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
795 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
797 if (sizeof($aSearch['aName']))
799 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
801 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
805 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
806 $aSearch['iSearchRank'] += 1000; // skip;
811 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
812 //$aSearch['iNamePhrase'] = $iPhrase;
814 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
818 if (isset($aValidTokens[$sToken]))
820 // Allow searching for a word - but at extra cost
821 foreach($aValidTokens[$sToken] as $aSearchTerm)
823 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
825 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
827 $aSearch = $aCurrentSearch;
828 $aSearch['iSearchRank'] += 1;
829 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
831 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
832 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
834 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
836 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
838 if (empty($aSearchTermToken['country_code'])
839 && empty($aSearchTermToken['lat'])
840 && empty($aSearchTermToken['class']))
842 $aSearch = $aCurrentSearch;
843 $aSearch['iSearchRank'] += 1;
844 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
845 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
851 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
852 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
856 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
858 $aSearch = $aCurrentSearch;
859 $aSearch['iSearchRank'] += 2;
860 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
861 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
862 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
864 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
865 $aSearch['iNamePhrase'] = $iPhrase;
866 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
873 // Allow skipping a word - but at EXTREAM cost
874 //$aSearch = $aCurrentSearch;
875 //$aSearch['iSearchRank']+=100;
876 //$aNewWordsetSearches[] = $aSearch;
880 usort($aNewWordsetSearches, 'bySearchRank');
881 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
883 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
885 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
886 usort($aNewPhraseSearches, 'bySearchRank');
888 $aSearchHash = array();
889 foreach($aNewPhraseSearches as $iSearch => $aSearch)
891 $sHash = serialize($aSearch);
892 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
893 else $aSearchHash[$sHash] = 1;
896 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
899 // Re-group the searches by their score, junk anything over 20 as just not worth trying
900 $aGroupedSearches = array();
901 foreach($aNewPhraseSearches as $aSearch)
903 if ($aSearch['iSearchRank'] < $this->iMaxRank)
905 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
906 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
909 ksort($aGroupedSearches);
912 $aSearches = array();
913 foreach($aGroupedSearches as $iScore => $aNewSearches)
915 $iSearchCount += sizeof($aNewSearches);
916 $aSearches = array_merge($aSearches, $aNewSearches);
917 if ($iSearchCount > 50) break;
920 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
927 // Re-group the searches by their score, junk anything over 20 as just not worth trying
928 $aGroupedSearches = array();
929 foreach($aSearches as $aSearch)
931 if ($aSearch['iSearchRank'] < $this->iMaxRank)
933 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
934 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
937 ksort($aGroupedSearches);
940 if (CONST_Debug) var_Dump($aGroupedSearches);
942 if ($this->bReverseInPlan)
944 $aCopyGroupedSearches = $aGroupedSearches;
945 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
947 foreach($aSearches as $iSearch => $aSearch)
949 if (sizeof($aSearch['aAddress']))
951 $iReverseItem = array_pop($aSearch['aAddress']);
952 if (isset($aPossibleMainWordIDs[$iReverseItem]))
954 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
955 $aSearch['aName'] = array($iReverseItem);
956 $aGroupedSearches[$iGroup][] = $aSearch;
958 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
959 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
965 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
967 $aCopyGroupedSearches = $aGroupedSearches;
968 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
970 foreach($aSearches as $iSearch => $aSearch)
972 $aReductionsList = array($aSearch['aAddress']);
973 $iSearchRank = $aSearch['iSearchRank'];
974 while(sizeof($aReductionsList) > 0)
977 if ($iSearchRank > iMaxRank) break 3;
978 $aNewReductionsList = array();
979 foreach($aReductionsList as $aReductionsWordList)
981 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
983 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
984 $aReverseSearch = $aSearch;
985 $aSearch['aAddress'] = $aReductionsWordListResult;
986 $aSearch['iSearchRank'] = $iSearchRank;
987 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
988 if (sizeof($aReductionsWordListResult) > 0)
990 $aNewReductionsList[] = $aReductionsWordListResult;
994 $aReductionsList = $aNewReductionsList;
998 ksort($aGroupedSearches);
1001 // Filter out duplicate searches
1002 $aSearchHash = array();
1003 foreach($aGroupedSearches as $iGroup => $aSearches)
1005 foreach($aSearches as $iSearch => $aSearch)
1007 $sHash = serialize($aSearch);
1008 if (isset($aSearchHash[$sHash]))
1010 unset($aGroupedSearches[$iGroup][$iSearch]);
1011 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1015 $aSearchHash[$sHash] = 1;
1020 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1024 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1027 foreach($aSearches as $aSearch)
1031 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1032 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1034 // No location term?
1035 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1037 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1039 // Just looking for a country by code - look it up
1040 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1042 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1043 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1044 $sSQL .= " order by st_area(geometry) desc limit 1";
1045 if (CONST_Debug) var_dump($sSQL);
1046 $aPlaceIDs = $this->oDB->getCol($sSQL);
1051 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1052 if (!$aSearch['sClass']) continue;
1053 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1054 if ($this->oDB->getOne($sSQL))
1056 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1057 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1058 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
1059 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1060 if (sizeof($this->aExcludePlaceIDs))
1062 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1064 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1065 $sSQL .= " limit $this->iLimit";
1066 if (CONST_Debug) var_dump($sSQL);
1067 $aPlaceIDs = $this->oDB->getCol($sSQL);
1069 // If excluded place IDs are given, it is fair to assume that
1070 // there have been results in the small box, so no further
1071 // expansion in that case.
1072 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1074 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1075 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1076 $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
1077 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1078 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1079 $sSQL .= " limit $this->iLimit";
1080 if (CONST_Debug) var_dump($sSQL);
1081 $aPlaceIDs = $this->oDB->getCol($sSQL);
1086 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1087 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
1088 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1089 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1090 $sSQL .= " limit $this->iLimit";
1091 if (CONST_Debug) var_dump($sSQL);
1092 $aPlaceIDs = $this->oDB->getCol($sSQL);
1098 $aPlaceIDs = array();
1100 // First we need a position, either aName or fLat or both
1104 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1105 // they might be right - but they are just too darned expensive to run
1106 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1107 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1108 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1110 // For infrequent name terms disable index usage for address
1111 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1112 sizeof($aSearch['aName']) == 1 &&
1113 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1115 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1119 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1120 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1123 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1124 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1125 if ($aSearch['fLon'] && $aSearch['fLat'])
1127 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1128 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1130 if (sizeof($this->aExcludePlaceIDs))
1132 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1134 if ($sCountryCodesSQL)
1136 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1139 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
1140 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1142 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1143 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1144 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1145 $aOrder[] = "$sImportanceSQL DESC";
1146 if (sizeof($aSearch['aFullNameAddress']))
1148 $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
1151 if (sizeof($aTerms))
1153 $sSQL = "select place_id";
1154 $sSQL .= " from search_name";
1155 $sSQL .= " where ".join(' and ',$aTerms);
1156 $sSQL .= " order by ".join(', ',$aOrder);
1157 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1158 $sSQL .= " limit 50";
1159 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1160 $sSQL .= " limit 1";
1162 $sSQL .= " limit ".$this->iLimit;
1164 if (CONST_Debug) { var_dump($sSQL); }
1165 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1166 if (PEAR::IsError($aViewBoxPlaceIDs))
1168 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1170 //var_dump($aViewBoxPlaceIDs);
1171 // Did we have an viewbox matches?
1172 $aPlaceIDs = array();
1173 $bViewBoxMatch = false;
1174 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1176 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1177 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1178 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1179 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1180 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1183 //var_Dump($aPlaceIDs);
1186 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1188 $aRoadPlaceIDs = $aPlaceIDs;
1189 $sPlaceIDs = join(',',$aPlaceIDs);
1191 // Now they are indexed look for a house attached to a street we found
1192 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1193 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1194 if (sizeof($this->aExcludePlaceIDs))
1196 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1198 $sSQL .= " limit $this->iLimit";
1199 if (CONST_Debug) var_dump($sSQL);
1200 $aPlaceIDs = $this->oDB->getCol($sSQL);
1202 // If not try the aux fallback table
1203 if (!sizeof($aPlaceIDs))
1205 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1206 if (sizeof($this->aExcludePlaceIDs))
1208 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1210 //$sSQL .= " limit $this->iLimit";
1211 if (CONST_Debug) var_dump($sSQL);
1212 $aPlaceIDs = $this->oDB->getCol($sSQL);
1215 if (!sizeof($aPlaceIDs))
1217 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1218 if (sizeof($this->aExcludePlaceIDs))
1220 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1222 //$sSQL .= " limit $this->iLimit";
1223 if (CONST_Debug) var_dump($sSQL);
1224 $aPlaceIDs = $this->oDB->getCol($sSQL);
1227 // Fallback to the road
1228 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1230 $aPlaceIDs = $aRoadPlaceIDs;
1235 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1237 $sPlaceIDs = join(',',$aPlaceIDs);
1238 $aClassPlaceIDs = array();
1240 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1242 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1243 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1244 $sSQL .= " and linked_place_id is null";
1245 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1246 $sSQL .= " order by rank_search asc limit $this->iLimit";
1247 if (CONST_Debug) var_dump($sSQL);
1248 $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1251 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1253 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1254 $bCacheTable = $this->oDB->getOne($sSQL);
1256 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1258 if (CONST_Debug) var_dump($sSQL);
1259 $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1261 // For state / country level searches the normal radius search doesn't work very well
1262 $sPlaceGeom = false;
1263 if ($this->iMaxRank < 9 && $bCacheTable)
1265 // Try and get a polygon to search in instead
1266 $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";
1267 if (CONST_Debug) var_dump($sSQL);
1268 $sPlaceGeom = $this->oDB->getOne($sSQL);
1277 $this->iMaxRank += 5;
1278 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1279 if (CONST_Debug) var_dump($sSQL);
1280 $aPlaceIDs = $this->oDB->getCol($sSQL);
1281 $sPlaceIDs = join(',',$aPlaceIDs);
1284 if ($sPlaceIDs || $sPlaceGeom)
1290 // More efficient - can make the range bigger
1294 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1295 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1296 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1298 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1299 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1302 $sSQL .= ",placex as f where ";
1303 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1308 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1310 if (sizeof($this->aExcludePlaceIDs))
1312 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1314 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1315 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1316 if ($iOffset) $sSQL .= " offset $iOffset";
1317 $sSQL .= " limit $this->iLimit";
1318 if (CONST_Debug) var_dump($sSQL);
1319 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1323 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1326 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1327 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1329 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1330 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1331 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1332 if (sizeof($this->aExcludePlaceIDs))
1334 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1336 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1337 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1338 if ($iOffset) $sSQL .= " offset $iOffset";
1339 $sSQL .= " limit $this->iLimit";
1340 if (CONST_Debug) var_dump($sSQL);
1341 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1346 $aPlaceIDs = $aClassPlaceIDs;
1352 if (PEAR::IsError($aPlaceIDs))
1354 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1357 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1359 foreach($aPlaceIDs as $iPlaceID)
1361 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1363 if ($iQueryLoop > 20) break;
1366 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1368 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1369 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1370 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1371 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1372 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1373 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1374 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1375 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1377 if (CONST_Debug) var_dump($sSQL);
1378 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1382 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1383 if ($iGroupLoop > 4) break;
1384 if ($iQueryLoop > 30) break;
1387 // Did we find anything?
1388 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1390 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1396 // Just interpret as a reverse geocode
1397 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1399 $aSearchResults = $this->getDetails(array($iPlaceID));
1401 $aSearchResults = array();
1405 if (!sizeof($aSearchResults))
1410 $aClassType = getClassTypesWithImportance();
1411 $aRecheckWords = preg_split('/\b/u',$sQuery);
1412 foreach($aRecheckWords as $i => $sWord)
1414 if (!$sWord) unset($aRecheckWords[$i]);
1417 foreach($aSearchResults as $iResNum => $aResult)
1419 if (CONST_Search_AreaPolygons)
1421 // Get the bounding box and outline polygon
1422 $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1423 $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1424 $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1425 $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1426 if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1427 if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1428 if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1429 if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1430 $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1431 $aPointPolygon = $this->oDB->getRow($sSQL);
1432 if (PEAR::IsError($aPointPolygon))
1434 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1437 if ($aPointPolygon['place_id'])
1439 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1440 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1441 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1442 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1444 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1446 $aResult['lat'] = $aPointPolygon['centrelat'];
1447 $aResult['lon'] = $aPointPolygon['centrelon'];
1450 if ($this->bIncludePolygonAsPoints)
1452 // Translate geometary string to point array
1453 if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1455 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1457 elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1459 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1461 elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1464 $iSteps = ($fRadius * 40000)^2;
1465 $fStepSize = (2*pi())/$iSteps;
1466 $aPolyPoints = array();
1467 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1469 $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1471 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1472 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1473 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1474 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1478 // Output data suitable for display (points and a bounding box)
1479 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1481 $aResult['aPolyPoints'] = array();
1482 foreach($aPolyPoints as $aPoint)
1484 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1487 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1491 if ($aResult['extra_place'] == 'city')
1493 $aResult['class'] = 'place';
1494 $aResult['type'] = 'city';
1495 $aResult['rank_search'] = 16;
1498 if (!isset($aResult['aBoundingBox']))
1501 $fDiameter = 0.0001;
1503 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1504 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1506 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1508 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1509 && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1511 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1513 $fRadius = $fDiameter / 2;
1515 $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1516 $fStepSize = (2*pi())/$iSteps;
1517 $aPolyPoints = array();
1518 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1520 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1522 $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1523 $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1524 $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1525 $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1527 // Output data suitable for display (points and a bounding box)
1528 if ($this->bIncludePolygonAsPoints)
1530 $aResult['aPolyPoints'] = array();
1531 foreach($aPolyPoints as $aPoint)
1533 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1536 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1539 // Is there an icon set for this type of result?
1540 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1541 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1543 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1546 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1547 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1549 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1551 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1552 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1554 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1557 if ($this->bIncludeAddressDetails)
1559 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1560 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1562 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1566 // Adjust importance for the number of exact string matches in the result
1567 $aResult['importance'] = max(0.001,$aResult['importance']);
1569 $sAddress = $aResult['langaddress'];
1570 foreach($aRecheckWords as $i => $sWord)
1572 if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1575 $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
1577 $aResult['name'] = $aResult['langaddress'];
1578 $aResult['foundorder'] = -$aResult['addressimportance'];
1579 $aSearchResults[$iResNum] = $aResult;
1581 uasort($aSearchResults, 'byImportance');
1583 $aOSMIDDone = array();
1584 $aClassTypeNameDone = array();
1585 $aToFilter = $aSearchResults;
1586 $aSearchResults = array();
1589 foreach($aToFilter as $iResNum => $aResult)
1591 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1592 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1595 $fLat = $aResult['lat'];
1596 $fLon = $aResult['lon'];
1597 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1600 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1601 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1603 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1604 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1605 $aSearchResults[] = $aResult;
1608 // Absolute limit on number of results
1609 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1612 return $aSearchResults;
1621 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1623 $aPoints = explode(',',$_GET['route']);
1624 if (sizeof($aPoints) % 2 != 0)
1626 userError("Uneven number of points");
1629 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1630 $fPrevCoord = false;