2 require_once(CONST_BasePath.'/lib/PlaceLookup.php');
3 require_once(CONST_BasePath.'/lib/ReverseGeocode.php');
9 protected $aLangPrefOrder = array();
11 protected $bIncludeAddressDetails = false;
12 protected $bIncludeExtraTags = false;
13 protected $bIncludeNameDetails = false;
15 protected $bIncludePolygonAsPoints = false;
16 protected $bIncludePolygonAsText = false;
17 protected $bIncludePolygonAsGeoJSON = false;
18 protected $bIncludePolygonAsKML = false;
19 protected $bIncludePolygonAsSVG = false;
20 protected $fPolygonSimplificationThreshold = 0.0;
22 protected $aExcludePlaceIDs = array();
23 protected $bDeDupe = true;
24 protected $bReverseInPlan = false;
26 protected $iLimit = 20;
27 protected $iFinalLimit = 10;
28 protected $iOffset = 0;
29 protected $bFallback = false;
31 protected $aCountryCodes = false;
32 protected $aNearPoint = false;
34 protected $bBoundedSearch = false;
35 protected $aViewBox = false;
36 protected $sViewboxCentreSQL = false;
37 protected $sViewboxSmallSQL = false;
38 protected $sViewboxLargeSQL = false;
40 protected $iMaxRank = 20;
41 protected $iMinAddressRank = 0;
42 protected $iMaxAddressRank = 30;
43 protected $aAddressRankList = array();
44 protected $exactMatchCache = array();
46 protected $sAllowedTypesSQLList = false;
48 protected $sQuery = false;
49 protected $aStructuredQuery = false;
51 function Geocode(&$oDB)
56 function setReverseInPlan($bReverse)
58 $this->bReverseInPlan = $bReverse;
61 function setLanguagePreference($aLangPref)
63 $this->aLangPrefOrder = $aLangPref;
66 function getIncludeAddressDetails()
68 return $this->bIncludeAddressDetails;
71 function getIncludeExtraTags()
73 return $this->bIncludeExtraTags;
76 function getIncludeNameDetails()
78 return $this->bIncludeNameDetails;
81 function setIncludePolygonAsPoints($b = true)
83 $this->bIncludePolygonAsPoints = $b;
86 function setIncludePolygonAsText($b = true)
88 $this->bIncludePolygonAsText = $b;
91 function setIncludePolygonAsGeoJSON($b = true)
93 $this->bIncludePolygonAsGeoJSON = $b;
96 function setIncludePolygonAsKML($b = true)
98 $this->bIncludePolygonAsKML = $b;
101 function setIncludePolygonAsSVG($b = true)
103 $this->bIncludePolygonAsSVG = $b;
106 function setPolygonSimplificationThreshold($f)
108 $this->fPolygonSimplificationThreshold = $f;
111 function setLimit($iLimit = 10)
113 if ($iLimit > 50) $iLimit = 50;
114 if ($iLimit < 1) $iLimit = 1;
116 $this->iFinalLimit = $iLimit;
117 $this->iLimit = $iLimit + min($iLimit, 10);
120 function getExcludedPlaceIDs()
122 return $this->aExcludePlaceIDs;
125 function getViewBoxString()
127 if (!$this->aViewBox) return null;
128 return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
131 function setFeatureType($sFeatureType)
133 switch ($sFeatureType) {
135 $this->setRankRange(4, 4);
138 $this->setRankRange(8, 8);
141 $this->setRankRange(14, 16);
144 $this->setRankRange(8, 20);
149 function setRankRange($iMin, $iMax)
151 $this->iMinAddressRank = $iMin;
152 $this->iMaxAddressRank = $iMax;
155 function setRoute($aRoutePoints, $fRouteWidth)
157 $this->aViewBox = false;
159 $this->sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
161 foreach ($this->aRoutePoints as $aPoint) {
162 $fPoint = (float)$aPoint;
163 $this->sViewboxCentreSQL .= $sSep.$fPoint;
164 $sSep = ($sSep == ' ') ? ',' : ' ';
166 $this->sViewboxCentreSQL .= ")'::geometry,4326)";
168 $this->sViewboxSmallSQL = 'st_buffer('.$this->sViewboxCentreSQL;
169 $this->sViewboxSmallSQL .= ','.($fRouteWidth/69).')';
171 $this->sViewboxLargeSQL = 'st_buffer('.$this->sViewboxCentreSQL;
172 $this->sViewboxLargeSQL .= ','.($fRouteWidth/30).')';
175 function setViewbox($aViewbox)
177 $this->aViewBox = array_map('floatval', $aViewbox);
179 $fHeight = $this->aViewBox[0] - $this->aViewBox[2];
180 $fWidth = $this->aViewBox[1] - $this->aViewBox[3];
181 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
182 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
183 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
184 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
186 $this->sViewboxCentreSQL = false;
187 $this->sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".$this->aViewBox[0].",".$this->aViewBox[1]."),ST_Point(".$this->aViewBox[2].",".$this->aViewBox[3].")),4326)";
188 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".$aBigViewBox[0].",".$aBigViewBox[1]."),ST_Point(".$aBigViewBox[2].",".$aBigViewBox[3].")),4326)";
191 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
193 $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
196 function setQuery($sQueryString)
198 $this->sQuery = $sQueryString;
199 $this->aStructuredQuery = false;
202 function getQueryString()
204 return $this->sQuery;
208 function loadParamArray($oParams)
210 $this->bIncludeAddressDetails
211 = $oParams->getBool('addressdetails', $this->bIncludeAddressDetails);
212 $this->bIncludeExtraTags
213 = $oParams->getBool('extratags', $this->bIncludeExtraTags);
214 $this->bIncludeNameDetails
215 = $oParams->getBool('namedetails', $this->bIncludeNameDetails);
217 $this->bBoundedSearch = $oParams->getBool('bounded', $this->bBoundedSearch);
218 $this->bDeDupe = $oParams->getBool('dedupe', $this->bDeDupe);
220 $this->setLimit($oParams->getInt('limit', $this->iFinalLimit));
221 $this->iOffset = $oParams->getInt('offset', $this->iOffset);
223 $this->bFallback = $oParams->getBool('fallback', $this->bFallback);
225 // List of excluded Place IDs - used for more acurate pageing
226 $sExcluded = $oParams->getStringList('exclude_place_ids');
228 foreach ($sExcluded as $iExcludedPlaceID) {
229 $iExcludedPlaceID = (int)$iExcludedPlaceID;
230 if ($iExcludedPlaceID)
231 $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
234 if (isset($aExcludePlaceIDs))
235 $this->aExcludePlaceIDs = $aExcludePlaceIDs;
238 // Only certain ranks of feature
239 $sFeatureType = $oParams->getString('featureType');
240 if (!$sFeatureType) $sFeatureType = $oParams->getString('featuretype');
241 if ($sFeatureType) $this->setFeatureType($sFeatureType);
244 $sCountries = $oParams->getStringList('countrycodes');
246 foreach ($sCountries as $sCountryCode) {
247 if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode)) {
248 $aCountries[] = strtolower($sCountryCode);
251 if (isset($aCountryCodes))
252 $this->aCountryCodes = $aCountries;
255 $aViewbox = $oParams->getStringList('viewboxlbrt');
257 $this->setViewbox($aViewbox);
259 $aViewbox = $oParams->getStringList('viewbox');
261 $this->setViewBox(array(
268 $aRoute = $oParams->getStringList('route');
269 $fRouteWidth = $oParams->getFloat('routewidth');
270 if ($aRoute && $fRouteWidth) {
271 $this->setRoute($aRoute, $fRouteWidth);
277 function setQueryFromParams($oParams)
280 $sQuery = $oParams->getString('q');
282 $this->setStructuredQuery(
283 $oParams->getString('amenity'),
284 $oParams->getString('street'),
285 $oParams->getString('city'),
286 $oParams->getString('county'),
287 $oParams->getString('state'),
288 $oParams->getString('country'),
289 $oParams->getString('postalcode')
291 $this->setReverseInPlan(false);
293 $this->setQuery($sQuery);
297 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
299 $sValue = trim($sValue);
300 if (!$sValue) return false;
301 $this->aStructuredQuery[$sKey] = $sValue;
302 if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30) {
303 $this->iMinAddressRank = $iNewMinAddressRank;
304 $this->iMaxAddressRank = $iNewMaxAddressRank;
306 if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
310 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
312 $this->sQuery = false;
315 $this->iMinAddressRank = 0;
316 $this->iMaxAddressRank = 30;
317 $this->aAddressRankList = array();
319 $this->aStructuredQuery = array();
320 $this->sAllowedTypesSQLList = '';
322 $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
323 $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
324 $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
325 $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
326 $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
327 $this->loadStructuredAddressElement($sPostalCode, 'postalcode', 5, 11, array(5, 11));
328 $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
330 if (sizeof($this->aStructuredQuery) > 0) {
331 $this->sQuery = join(', ', $this->aStructuredQuery);
332 if ($this->iMaxAddressRank < 30) {
333 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
338 function fallbackStructuredQuery()
340 if (!$this->aStructuredQuery) return false;
342 $aParams = $this->aStructuredQuery;
344 if (sizeof($aParams) == 1) return false;
346 $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
348 foreach ($aOrderToFallback as $sType) {
349 if (isset($aParams[$sType])) {
350 unset($aParams[$sType]);
351 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
359 function getDetails($aPlaceIDs)
361 //$aPlaceIDs is an array with key: placeID and value: tiger-housenumber, if found, else -1
362 if (sizeof($aPlaceIDs) == 0) return array();
364 $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
366 // Get the details for display (is this a redundant extra step?)
367 $sPlaceIDs = join(',', array_keys($aPlaceIDs));
369 $sImportanceSQL = '';
370 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
371 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
373 $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id, calculated_country_code as country_code,";
374 $sSQL .= "get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) as langaddress,";
375 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
376 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
377 if ($this->bIncludeExtraTags) $sSQL .= "hstore_to_json(extratags)::text as extra,";
378 if ($this->bIncludeNameDetails) $sSQL .= "hstore_to_json(name)::text as names,";
379 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
380 $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
381 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
382 $sSQL .= "(extratags->'place') as extra_place ";
383 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
384 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
385 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
386 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',', $this->aAddressRankList).")";
388 if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
389 $sSQL .= "and linked_place_id is null ";
390 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
391 if (!$this->bDeDupe) $sSQL .= ",place_id";
392 $sSQL .= ",langaddress ";
393 $sSQL .= ",placename ";
395 if ($this->bIncludeExtraTags) $sSQL .= ",extratags";
396 if ($this->bIncludeNameDetails) $sSQL .= ",name";
397 $sSQL .= ",extratags->'place' ";
399 if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank) {
400 //only Tiger housenumbers and interpolation lines need to be interpolated, because they are saved as lines
401 // with start- and endnumber, the common osm housenumbers are usually saved as points
404 $length = count($aPlaceIDs);
405 foreach ($aPlaceIDs as $placeID => $housenumber) {
407 $sHousenumbers .= "(".$placeID.", ".$housenumber.")";
408 if ($i<$length) $sHousenumbers .= ", ";
410 if (CONST_Use_US_Tiger_Data) {
411 //Tiger search only if a housenumber was searched and if it was found (i.e. aPlaceIDs[placeID] = housenumber != -1) (realized through a join)
413 $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, min(parent_place_id) as parent_place_id, 'us' as country_code";
414 $sSQL .= ", get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) as langaddress ";
415 $sSQL .= ", null as placename";
416 $sSQL .= ", null as ref";
417 if ($this->bIncludeExtraTags) $sSQL .= ", null as extra";
418 if ($this->bIncludeNameDetails) $sSQL .= ", null as names";
419 $sSQL .= ", avg(st_x(centroid)) as lon, avg(st_y(centroid)) as lat,";
420 $sSQL .= $sImportanceSQL."-1.15 as importance ";
421 $sSQL .= ", (select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(blub.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance ";
422 $sSQL .= ", null as extra_place ";
423 $sSQL .= " from (select place_id";
424 //interpolate the Tiger housenumbers here
425 $sSQL .= ", ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) as centroid, parent_place_id, housenumber_for_place";
426 $sSQL .= " from (location_property_tiger ";
427 $sSQL .= " join (values ".$sHousenumbers.") as housenumbers(place_id, housenumber_for_place) using(place_id)) ";
428 $sSQL .= " where housenumber_for_place>=0 and 30 between $this->iMinAddressRank and $this->iMaxAddressRank) as blub"; //postgres wants an alias here
429 $sSQL .= " group by place_id, housenumber_for_place"; //is this group by really needed?, place_id + housenumber (in combination) are unique
430 if (!$this->bDeDupe) $sSQL .= ", place_id ";
433 // interpolation line search only if a housenumber was searched and if it was found (i.e. aPlaceIDs[placeID] = housenumber != -1) (realized through a join)
435 $sSQL .= "select 'W' 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, min(parent_place_id) as parent_place_id, calculated_country_code as country_code, ";
436 $sSQL .= "get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) as langaddress, ";
437 $sSQL .= "null as placename, ";
438 $sSQL .= "null as ref, ";
439 if ($this->bIncludeExtraTags) $sSQL .= "null as extra, ";
440 if ($this->bIncludeNameDetails) $sSQL .= "null as names, ";
441 $sSQL .= " avg(st_x(centroid)) as lon, avg(st_y(centroid)) as lat,";
442 $sSQL .= $sImportanceSQL."-0.1 as importance, "; // slightly smaller than the importance for normal houses with rank 30, which is 0
443 $sSQL .= " (select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p";
444 $sSQL .= " where s.place_id = min(blub.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance,";
445 $sSQL .= " null as extra_place ";
446 $sSQL .= " from (select place_id, calculated_country_code ";
447 //interpolate the housenumbers here
448 $sSQL .= ", CASE WHEN startnumber != endnumber THEN ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) ";
449 $sSQL .= " ELSE ST_LineInterpolatePoint(linegeo, 0.5) END as centroid";
450 $sSQL .= ", parent_place_id, housenumber_for_place ";
451 $sSQL .= " from (location_property_osmline ";
452 $sSQL .= " join (values ".$sHousenumbers.") as housenumbers(place_id, housenumber_for_place) using(place_id)) ";
453 $sSQL .= " where housenumber_for_place>=0 and 30 between $this->iMinAddressRank and $this->iMaxAddressRank) as blub"; //postgres wants an alias here
454 $sSQL .= " group by place_id, housenumber_for_place, calculated_country_code "; //is this group by really needed?, place_id + housenumber (in combination) are unique
455 if (!$this->bDeDupe) $sSQL .= ", place_id ";
457 if (CONST_Use_Aux_Location_data) {
459 $sSQL .= "select 'L' as osm_type, place_id as osm_id, 'place' as class, 'house' as type, null as admin_level, 0 as rank_search, 0 as rank_address, min(place_id) as place_id, min(parent_place_id) as parent_place_id, 'us' as country_code, ";
460 $sSQL .= "get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) as langaddress, ";
461 $sSQL .= "null as placename, ";
462 $sSQL .= "null as ref, ";
463 if ($this->bIncludeExtraTags) $sSQL .= "null as extra, ";
464 if ($this->bIncludeNameDetails) $sSQL .= "null as names, ";
465 $sSQL .= "avg(ST_X(centroid)) as lon, avg(ST_Y(centroid)) as lat, ";
466 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
467 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
468 $sSQL .= "null as extra_place ";
469 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
470 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
471 $sSQL .= "group by place_id";
472 if (!$this->bDeDupe) $sSQL .= ", place_id";
473 $sSQL .= ", get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) ";
477 $sSQL .= " order by importance desc";
479 echo "<hr>"; var_dump($sSQL);
481 $aSearchResults = chksql(
482 $this->oDB->getAll($sSQL),
483 "Could not get details for place."
486 return $aSearchResults;
489 function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
492 Calculate all searches using aValidTokens i.e.
493 'Wodsworth Road, Sheffield' =>
497 0 1 (wodsworth)(road)
500 Score how good the search is so they can be ordered
502 foreach ($aPhrases as $iPhrase => $sPhrase) {
503 $aNewPhraseSearches = array();
504 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
505 else $sPhraseType = '';
507 foreach ($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset) {
508 // Too many permutations - too expensive
509 if ($iWordSet > 120) break;
511 $aWordsetSearches = $aSearches;
513 // Add all words from this wordset
514 foreach ($aWordset as $iToken => $sToken) {
515 //echo "<br><b>$sToken</b>";
516 $aNewWordsetSearches = array();
518 foreach ($aWordsetSearches as $aCurrentSearch) {
520 //var_dump($aCurrentSearch);
523 // If the token is valid
524 if (isset($aValidTokens[' '.$sToken])) {
525 foreach ($aValidTokens[' '.$sToken] as $aSearchTerm) {
526 $aSearch = $aCurrentSearch;
527 $aSearch['iSearchRank']++;
528 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0') {
529 if ($aSearch['sCountryCode'] === false) {
530 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
531 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
532 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases))) {
533 $aSearch['iSearchRank'] += 5;
535 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
537 } elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null) {
538 if ($aSearch['fLat'] === '') {
539 $aSearch['fLat'] = $aSearchTerm['lat'];
540 $aSearch['fLon'] = $aSearchTerm['lon'];
541 $aSearch['fRadius'] = $aSearchTerm['radius'];
542 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
544 } elseif ($sPhraseType == 'postalcode') {
545 // 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
546 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
547 // If we already have a name try putting the postcode first
548 if (sizeof($aSearch['aName'])) {
549 $aNewSearch = $aSearch;
550 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
551 $aNewSearch['aName'] = array();
552 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
553 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
556 if (sizeof($aSearch['aName'])) {
557 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) {
558 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
560 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
561 $aSearch['iSearchRank'] += 1000; // skip;
564 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
565 //$aSearch['iNamePhrase'] = $iPhrase;
567 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
569 } elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house') {
570 if ($aSearch['sHouseNumber'] === '') {
571 $aSearch['sHouseNumber'] = $sToken;
572 // sanity check: if the housenumber is not mainly made
573 // up of numbers, add a penalty
574 if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
575 // also housenumbers should appear in the first or second phrase
576 if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
577 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
579 // Fall back to not searching for this item (better than nothing)
580 $aSearch = $aCurrentSearch;
581 $aSearch['iSearchRank'] += 1;
582 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
585 } elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null) {
586 if ($aSearch['sClass'] === '') {
587 $aSearch['sOperator'] = $aSearchTerm['operator'];
588 $aSearch['sClass'] = $aSearchTerm['class'];
589 $aSearch['sType'] = $aSearchTerm['type'];
590 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
591 else $aSearch['sOperator'] = 'near'; // near = in for the moment
592 if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
594 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
596 } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
597 if (sizeof($aSearch['aName'])) {
598 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) {
599 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
601 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
602 $aSearch['iSearchRank'] += 1000; // skip;
605 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
606 //$aSearch['iNamePhrase'] = $iPhrase;
608 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
612 // Look for partial matches.
613 // Note that there is no point in adding country terms here
614 // because country are omitted in the address.
615 if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country') {
616 // Allow searching for a word - but at extra cost
617 foreach ($aValidTokens[$sToken] as $aSearchTerm) {
618 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
619 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false) {
620 $aSearch = $aCurrentSearch;
621 $aSearch['iSearchRank'] += 1;
622 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
623 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
624 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
625 } elseif (isset($aValidTokens[' '.$sToken])) { // revert to the token version?
626 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
627 $aSearch['iSearchRank'] += 1;
628 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
629 foreach ($aValidTokens[' '.$sToken] as $aSearchTermToken) {
630 if (empty($aSearchTermToken['country_code'])
631 && empty($aSearchTermToken['lat'])
632 && empty($aSearchTermToken['class'])
634 $aSearch = $aCurrentSearch;
635 $aSearch['iSearchRank'] += 1;
636 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
637 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
641 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
642 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
643 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
647 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase) {
648 $aSearch = $aCurrentSearch;
649 $aSearch['iSearchRank'] += 1;
650 if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
651 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
652 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
653 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
655 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
657 $aSearch['iNamePhrase'] = $iPhrase;
658 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
663 // Allow skipping a word - but at EXTREAM cost
664 //$aSearch = $aCurrentSearch;
665 //$aSearch['iSearchRank']+=100;
666 //$aNewWordsetSearches[] = $aSearch;
670 usort($aNewWordsetSearches, 'bySearchRank');
671 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
673 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
675 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
676 usort($aNewPhraseSearches, 'bySearchRank');
678 $aSearchHash = array();
679 foreach ($aNewPhraseSearches as $iSearch => $aSearch) {
680 $sHash = serialize($aSearch);
681 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
682 else $aSearchHash[$sHash] = 1;
685 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
688 // Re-group the searches by their score, junk anything over 20 as just not worth trying
689 $aGroupedSearches = array();
690 foreach ($aNewPhraseSearches as $aSearch) {
691 if ($aSearch['iSearchRank'] < $this->iMaxRank) {
692 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
693 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
696 ksort($aGroupedSearches);
699 $aSearches = array();
700 foreach ($aGroupedSearches as $iScore => $aNewSearches) {
701 $iSearchCount += sizeof($aNewSearches);
702 $aSearches = array_merge($aSearches, $aNewSearches);
703 if ($iSearchCount > 50) break;
706 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
708 return $aGroupedSearches;
711 /* Perform the actual query lookup.
713 Returns an ordered list of results, each with the following fields:
714 osm_type: type of corresponding OSM object
718 P - postcode (internally computed)
719 osm_id: id of corresponding OSM object
720 class: general object class (corresponds to tag key of primary OSM tag)
721 type: subclass of object (corresponds to tag value of primary OSM tag)
722 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
723 rank_search: rank in search hierarchy
724 (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
725 rank_address: rank in address hierarchy (determines orer in address)
726 place_id: internal key (may differ between different instances)
727 country_code: ISO country code
728 langaddress: localized full address
729 placename: localized name of object
730 ref: content of ref tag (if available)
733 importance: importance of place based on Wikipedia link count
734 addressimportance: cumulated importance of address elements
735 extra_place: type of place (for admin boundaries, if there is a place tag)
736 aBoundingBox: bounding Box
737 label: short description of the object class/type (English only)
738 name: full name (currently the same as langaddress)
739 foundorder: secondary ordering for places with same importance
743 if (!$this->sQuery && !$this->aStructuredQuery) return false;
745 $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
746 $sCountryCodesSQL = false;
747 if ($this->aCountryCodes) {
748 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
751 $sQuery = $this->sQuery;
753 // Conflicts between US state abreviations and various words for 'the' in different languages
754 if (isset($this->aLangPrefOrder['name:en'])) {
755 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/', '\1illinois\2', $sQuery);
756 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/', '\1alabama\2', $sQuery);
757 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/', '\1louisiana\2', $sQuery);
760 $bBoundingBoxSearch = $this->bBoundedSearch && $this->sViewboxSmallSQL;
761 if ($this->sViewboxCentreSQL) {
762 // For complex viewboxes (routes) precompute the bounding geometry
764 $this->oDB->getOne("select ".$this->sViewboxSmallSQL),
765 "Could not get small viewbox"
767 $this->sViewboxSmallSQL = "'".$sGeom."'::geometry";
770 $this->oDB->getOne("select ".$this->sViewboxLargeSQL),
771 "Could not get large viewbox"
773 $this->sViewboxLargeSQL = "'".$sGeom."'::geometry";
776 // Do we have anything that looks like a lat/lon pair?
777 if ($aLooksLike = looksLikeLatLonPair($sQuery)) {
778 $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
779 $sQuery = $aLooksLike['query'];
782 $aSearchResults = array();
783 if ($sQuery || $this->aStructuredQuery) {
784 // Start with a blank search
789 'sCountryCode' => false,
791 'aAddress' => array(),
792 'aFullNameAddress' => array(),
793 'aNameNonSearch' => array(),
794 'aAddressNonSearch' => array(),
796 'aFeatureName' => array(),
799 'sHouseNumber' => '',
806 // Do we have a radius search?
807 $sNearPointSQL = false;
808 if ($this->aNearPoint) {
809 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
810 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
811 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
812 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
815 // Any 'special' terms in the search?
816 $bSpecialTerms = false;
817 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
818 $aSpecialTerms = array();
819 foreach ($aSpecialTermsRaw as $aSpecialTerm) {
820 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
821 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
824 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
825 $aSpecialTerms = array();
826 if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity']) {
827 $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
828 unset($this->aStructuredQuery['amenity']);
831 foreach ($aSpecialTermsRaw as $aSpecialTerm) {
832 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
833 $sToken = chksql($this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string"));
834 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
835 $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';
836 if (CONST_Debug) var_Dump($sSQL);
837 $aSearchWords = chksql($this->oDB->getAll($sSQL));
838 $aNewSearches = array();
839 foreach ($aSearches as $aSearch) {
840 foreach ($aSearchWords as $aSearchTerm) {
841 $aNewSearch = $aSearch;
842 if ($aSearchTerm['country_code']) {
843 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
844 $aNewSearches[] = $aNewSearch;
845 $bSpecialTerms = true;
847 if ($aSearchTerm['class']) {
848 $aNewSearch['sClass'] = $aSearchTerm['class'];
849 $aNewSearch['sType'] = $aSearchTerm['type'];
850 $aNewSearches[] = $aNewSearch;
851 $bSpecialTerms = true;
855 $aSearches = $aNewSearches;
858 // Split query into phrases
859 // Commas are used to reduce the search space by indicating where phrases split
860 if ($this->aStructuredQuery) {
861 $aPhrases = $this->aStructuredQuery;
862 $bStructuredPhrases = true;
864 $aPhrases = explode(',', $sQuery);
865 $bStructuredPhrases = false;
868 // Convert each phrase to standard form
869 // Create a list of standard words
870 // Get all 'sets' of words
871 // Generate a complete list of all
873 foreach ($aPhrases as $iPhrase => $sPhrase) {
875 $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string"),
876 "Cannot nomralize query string (is it an UTF-8 string?)"
878 if (trim($aPhrase['string'])) {
879 $aPhrases[$iPhrase] = $aPhrase;
880 $aPhrases[$iPhrase]['words'] = explode(' ', $aPhrases[$iPhrase]['string']);
881 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
882 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
884 unset($aPhrases[$iPhrase]);
888 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
889 $aPhraseTypes = array_keys($aPhrases);
890 $aPhrases = array_values($aPhrases);
892 if (sizeof($aTokens)) {
893 // Check which tokens we have, get the ID numbers
894 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
895 $sSQL .= ' from word where word_token in ('.join(',', array_map("getDBQuoted", $aTokens)).')';
897 if (CONST_Debug) var_Dump($sSQL);
899 $aValidTokens = array();
900 if (sizeof($aTokens)) {
901 $aDatabaseWords = chksql(
902 $this->oDB->getAll($sSQL),
903 "Could not get word tokens."
906 $aDatabaseWords = array();
908 $aPossibleMainWordIDs = array();
909 $aWordFrequencyScores = array();
910 foreach ($aDatabaseWords as $aToken) {
911 // Very special case - require 2 letter country param to match the country code found
912 if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
913 && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code']
918 if (isset($aValidTokens[$aToken['word_token']])) {
919 $aValidTokens[$aToken['word_token']][] = $aToken;
921 $aValidTokens[$aToken['word_token']] = array($aToken);
923 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
924 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
926 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
928 // Try and calculate GB postcodes we might be missing
929 foreach ($aTokens as $sToken) {
930 // Source of gb postcodes is now definitive - always use
931 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData)) {
932 if (substr($aData[1], -2, 1) != ' ') {
933 $aData[0] = substr($aData[0], 0, strlen($aData[1])-1).' '.substr($aData[0], strlen($aData[1])-1);
934 $aData[1] = substr($aData[1], 0, -1).' '.substr($aData[1], -1, 1);
936 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
937 if ($aGBPostcodeLocation) {
938 $aValidTokens[$sToken] = $aGBPostcodeLocation;
940 } elseif (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData)) {
941 // US ZIP+4 codes - if there is no token,
942 // merge in the 5-digit ZIP code
943 if (isset($aValidTokens[$aData[1]])) {
944 foreach ($aValidTokens[$aData[1]] as $aToken) {
945 if (!$aToken['class']) {
946 if (isset($aValidTokens[$sToken])) {
947 $aValidTokens[$sToken][] = $aToken;
949 $aValidTokens[$sToken] = array($aToken);
957 foreach ($aTokens as $sToken) {
958 // Unknown single word token with a number - assume it is a house number
959 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken, ' ') === false && preg_match('/[0-9]/', $sToken)) {
960 $aValidTokens[' '.$sToken] = array(array('class' => 'place', 'type' => 'house'));
964 // Any words that have failed completely?
967 // Start the search process
968 // array with: placeid => -1 | tiger-housenumber
969 $aResultPlaceIDs = array();
971 $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
973 if ($this->bReverseInPlan) {
974 // Reverse phrase array and also reverse the order of the wordsets in
975 // the first and final phrase. Don't bother about phrases in the middle
976 // because order in the address doesn't matter.
977 $aPhrases = array_reverse($aPhrases);
978 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
979 if (sizeof($aPhrases) > 1) {
980 $aFinalPhrase = end($aPhrases);
981 $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
983 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
985 foreach ($aGroupedSearches as $aSearches) {
986 foreach ($aSearches as $aSearch) {
987 if ($aSearch['iSearchRank'] < $this->iMaxRank) {
988 if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
989 $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
994 $aGroupedSearches = $aReverseGroupedSearches;
995 ksort($aGroupedSearches);
998 // Re-group the searches by their score, junk anything over 20 as just not worth trying
999 $aGroupedSearches = array();
1000 foreach ($aSearches as $aSearch) {
1001 if ($aSearch['iSearchRank'] < $this->iMaxRank) {
1002 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1003 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1006 ksort($aGroupedSearches);
1009 if (CONST_Debug) var_Dump($aGroupedSearches);
1010 if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0) {
1011 $aCopyGroupedSearches = $aGroupedSearches;
1012 foreach ($aCopyGroupedSearches as $iGroup => $aSearches) {
1013 foreach ($aSearches as $iSearch => $aSearch) {
1014 $aReductionsList = array($aSearch['aAddress']);
1015 $iSearchRank = $aSearch['iSearchRank'];
1016 while (sizeof($aReductionsList) > 0) {
1018 if ($iSearchRank > iMaxRank) break 3;
1019 $aNewReductionsList = array();
1020 foreach ($aReductionsList as $aReductionsWordList) {
1021 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++) {
1022 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1023 $aReverseSearch = $aSearch;
1024 $aSearch['aAddress'] = $aReductionsWordListResult;
1025 $aSearch['iSearchRank'] = $iSearchRank;
1026 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1027 if (sizeof($aReductionsWordListResult) > 0) {
1028 $aNewReductionsList[] = $aReductionsWordListResult;
1032 $aReductionsList = $aNewReductionsList;
1036 ksort($aGroupedSearches);
1039 // Filter out duplicate searches
1040 $aSearchHash = array();
1041 foreach ($aGroupedSearches as $iGroup => $aSearches) {
1042 foreach ($aSearches as $iSearch => $aSearch) {
1043 $sHash = serialize($aSearch);
1044 if (isset($aSearchHash[$sHash])) {
1045 unset($aGroupedSearches[$iGroup][$iSearch]);
1046 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1048 $aSearchHash[$sHash] = 1;
1053 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1057 foreach ($aGroupedSearches as $iGroupedRank => $aSearches) {
1059 foreach ($aSearches as $aSearch) {
1061 $searchedHousenumber = -1;
1063 if (CONST_Debug) echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>";
1064 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1066 // No location term?
1067 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon']) {
1068 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber']) {
1069 // Just looking for a country by code - look it up
1070 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank) {
1071 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1072 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1073 if ($bBoundingBoxSearch)
1074 $sSQL .= " and _st_intersects($this->sViewboxSmallSQL, geometry)";
1075 $sSQL .= " order by st_area(geometry) desc limit 1";
1076 if (CONST_Debug) var_dump($sSQL);
1077 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1079 $aPlaceIDs = array();
1082 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1083 if (!$aSearch['sClass']) continue;
1085 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1086 if (chksql($this->oDB->getOne($sSQL))) {
1087 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1088 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1089 $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1090 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1091 if (sizeof($this->aExcludePlaceIDs)) {
1092 $sSQL .= " and place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1094 if ($this->sViewboxCentreSQL) $sSQL .= " order by st_distance($this->sViewboxCentreSQL, ct.centroid) asc";
1095 $sSQL .= " limit $this->iLimit";
1096 if (CONST_Debug) var_dump($sSQL);
1097 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1099 // If excluded place IDs are given, it is fair to assume that
1100 // there have been results in the small box, so no further
1101 // expansion in that case.
1102 // Also don't expand if bounded results were requested.
1103 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch) {
1104 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1105 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1106 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1107 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1108 if ($this->sViewboxCentreSQL) $sSQL .= " order by st_distance($this->sViewboxCentreSQL, ct.centroid) asc";
1109 $sSQL .= " limit $this->iLimit";
1110 if (CONST_Debug) var_dump($sSQL);
1111 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1114 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1115 $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1116 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1117 if ($this->sViewboxCentreSQL) $sSQL .= " order by st_distance($this->sViewboxCentreSQL, centroid) asc";
1118 $sSQL .= " limit $this->iLimit";
1119 if (CONST_Debug) var_dump($sSQL);
1120 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1123 } elseif ($aSearch['fLon'] && !sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['sClass']) {
1124 // If a coordinate is given, the search must either
1125 // be for a name or a special search. Ignore everythin else.
1126 $aPlaceIDs = array();
1128 $aPlaceIDs = array();
1130 // First we need a position, either aName or fLat or both
1134 if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress'])) {
1135 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1137 $aOrder[0] = " (exists(select place_id from placex where parent_place_id = search_name.place_id";
1138 $aOrder[0] .= " and transliteration(housenumber) ~* E'".$sHouseNumberRegex."' limit 1) ";
1139 // also housenumbers from interpolation lines table are needed
1140 $aOrder[0] .= " or exists(select place_id from location_property_osmline where parent_place_id = search_name.place_id";
1141 $aOrder[0] .= " and ".intval($aSearch['sHouseNumber']).">=startnumber and ".intval($aSearch['sHouseNumber'])."<=endnumber limit 1))";
1142 $aOrder[0] .= " desc";
1145 // TODO: filter out the pointless search terms (2 letter name tokens and less)
1146 // they might be right - but they are just too darned expensive to run
1147 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'], ",")."]";
1148 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'], ",")."]";
1149 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress']) {
1150 // For infrequent name terms disable index usage for address
1151 if (CONST_Search_NameOnlySearchFrequencyThreshold
1152 && sizeof($aSearch['aName']) == 1
1153 && $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold
1155 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'], $aSearch['aAddressNonSearch']), ",")."]";
1157 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'], ",")."]";
1158 if (sizeof($aSearch['aAddressNonSearch'])) {
1159 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'], ",")."]";
1163 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1164 if ($aSearch['sHouseNumber']) {
1165 $aTerms[] = "address_rank between 16 and 27";
1167 if ($this->iMinAddressRank > 0) {
1168 $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1170 if ($this->iMaxAddressRank < 30) {
1171 $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1174 if ($aSearch['fLon'] && $aSearch['fLat']) {
1175 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1176 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1178 if (sizeof($this->aExcludePlaceIDs)) {
1179 $aTerms[] = "place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1181 if ($sCountryCodesSQL) {
1182 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1185 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1186 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1188 if ($aSearch['sHouseNumber']) {
1189 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1191 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1193 if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1194 if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1196 $aOrder[] = "$sImportanceSQL DESC";
1197 if (sizeof($aSearch['aFullNameAddress'])) {
1198 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'], ",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1199 $aOrder[] = 'exactmatch DESC';
1201 $sExactMatchSQL = '0::int as exactmatch';
1204 if (sizeof($aTerms)) {
1205 $sSQL = "select place_id, ";
1206 $sSQL .= $sExactMatchSQL;
1207 $sSQL .= " from search_name";
1208 $sSQL .= " where ".join(' and ', $aTerms);
1209 $sSQL .= " order by ".join(', ', $aOrder);
1210 if ($aSearch['sHouseNumber'] || $aSearch['sClass']) {
1211 $sSQL .= " limit 20";
1212 } elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass']) {
1213 $sSQL .= " limit 1";
1215 $sSQL .= " limit ".$this->iLimit;
1218 if (CONST_Debug) var_dump($sSQL);
1219 $aViewBoxPlaceIDs = chksql(
1220 $this->oDB->getAll($sSQL),
1221 "Could not get places for search terms."
1223 //var_dump($aViewBoxPlaceIDs);
1224 // Did we have an viewbox matches?
1225 $aPlaceIDs = array();
1226 $bViewBoxMatch = false;
1227 foreach ($aViewBoxPlaceIDs as $aViewBoxRow) {
1228 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1229 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1230 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1231 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1232 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1233 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1236 //var_Dump($aPlaceIDs);
1239 //now search for housenumber, if housenumber provided
1240 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs)) {
1241 $searchedHousenumber = intval($aSearch['sHouseNumber']);
1242 $aRoadPlaceIDs = $aPlaceIDs;
1243 $sPlaceIDs = join(',', $aPlaceIDs);
1245 // Now they are indexed, look for a house attached to a street we found
1246 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1247 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1248 if (sizeof($this->aExcludePlaceIDs)) {
1249 $sSQL .= " and place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1251 $sSQL .= " limit $this->iLimit";
1252 if (CONST_Debug) var_dump($sSQL);
1253 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1255 // if nothing found, search in the interpolation line table
1256 if (!sizeof($aPlaceIDs)) {
1257 // do we need to use transliteration and the regex for housenumbers???
1258 //new query for lines, not housenumbers anymore
1259 if ($searchedHousenumber%2 == 0) {
1260 //if housenumber is even, look for housenumber in streets with interpolationtype even or all
1261 $sSQL = "select distinct place_id from location_property_osmline where parent_place_id in (".$sPlaceIDs.") and (interpolationtype='even' or interpolationtype='all') and ".$searchedHousenumber.">=startnumber and ".$searchedHousenumber."<=endnumber";
1263 //look for housenumber in streets with interpolationtype odd or all
1264 $sSQL = "select distinct place_id from location_property_osmline where parent_place_id in (".$sPlaceIDs.") and (interpolationtype='odd' or interpolationtype='all') and ".$searchedHousenumber.">=startnumber and ".$searchedHousenumber."<=endnumber";
1267 if (sizeof($this->aExcludePlaceIDs)) {
1268 $sSQL .= " and place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1270 //$sSQL .= " limit $this->iLimit";
1271 if (CONST_Debug) var_dump($sSQL);
1273 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
1276 // If nothing found try the aux fallback table
1277 if (CONST_Use_Aux_Location_data && !sizeof($aPlaceIDs)) {
1278 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1279 if (sizeof($this->aExcludePlaceIDs)) {
1280 $sSQL .= " and parent_place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1282 //$sSQL .= " limit $this->iLimit";
1283 if (CONST_Debug) var_dump($sSQL);
1284 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1287 //if nothing was found in placex or location_property_aux, then search in Tiger data for this housenumber(location_property_tiger)
1288 if (CONST_Use_US_Tiger_Data && !sizeof($aPlaceIDs)) {
1289 //new query for lines, not housenumbers anymore
1290 if ($searchedHousenumber%2 == 0) {
1291 //if housenumber is even, look for housenumber in streets with interpolationtype even or all
1292 $sSQL = "select distinct place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and (interpolationtype='even' or interpolationtype='all') and ".$searchedHousenumber.">=startnumber and ".$searchedHousenumber."<=endnumber";
1294 //look for housenumber in streets with interpolationtype odd or all
1295 $sSQL = "select distinct place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and (interpolationtype='odd' or interpolationtype='all') and ".$searchedHousenumber.">=startnumber and ".$searchedHousenumber."<=endnumber";
1298 if (sizeof($this->aExcludePlaceIDs)) {
1299 $sSQL .= " and place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1301 //$sSQL .= " limit $this->iLimit";
1302 if (CONST_Debug) var_dump($sSQL);
1304 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
1307 // Fallback to the road (if no housenumber was found)
1308 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber'])) {
1309 $aPlaceIDs = $aRoadPlaceIDs;
1310 //set to -1, if no housenumbers were found
1311 $searchedHousenumber = -1;
1313 //else: housenumber was found, remains saved in searchedHousenumber
1317 if ($aSearch['sClass'] && sizeof($aPlaceIDs)) {
1318 $sPlaceIDs = join(',', $aPlaceIDs);
1319 $aClassPlaceIDs = array();
1321 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name') {
1322 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1323 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1324 $sSQL .= " and linked_place_id is null";
1325 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1326 $sSQL .= " order by rank_search asc limit $this->iLimit";
1327 if (CONST_Debug) var_dump($sSQL);
1328 $aClassPlaceIDs = chksql($this->oDB->getCol($sSQL));
1331 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') { // & in
1332 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1333 $bCacheTable = chksql($this->oDB->getOne($sSQL));
1335 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1337 if (CONST_Debug) var_dump($sSQL);
1338 $this->iMaxRank = ((int)chksql($this->oDB->getOne($sSQL)));
1340 // For state / country level searches the normal radius search doesn't work very well
1341 $sPlaceGeom = false;
1342 if ($this->iMaxRank < 9 && $bCacheTable) {
1343 // Try and get a polygon to search in instead
1344 $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";
1345 if (CONST_Debug) var_dump($sSQL);
1346 $sPlaceGeom = chksql($this->oDB->getOne($sSQL));
1352 $this->iMaxRank += 5;
1353 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1354 if (CONST_Debug) var_dump($sSQL);
1355 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1356 $sPlaceIDs = join(',', $aPlaceIDs);
1359 if ($sPlaceIDs || $sPlaceGeom) {
1362 // More efficient - can make the range bigger
1366 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1367 elseif ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1368 elseif ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1370 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1371 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1373 $sSQL .= ",placex as f where ";
1374 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1378 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1380 if (sizeof($this->aExcludePlaceIDs)) {
1381 $sSQL .= " and l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1383 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1384 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1385 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1386 $sSQL .= " limit $this->iLimit";
1387 if (CONST_Debug) var_dump($sSQL);
1388 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
1390 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1393 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1394 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1396 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1397 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1398 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1399 if (sizeof($this->aExcludePlaceIDs)) {
1400 $sSQL .= " and l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1402 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1403 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1404 if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1405 $sSQL .= " limit $this->iLimit";
1406 if (CONST_Debug) var_dump($sSQL);
1407 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
1411 $aPlaceIDs = $aClassPlaceIDs;
1416 echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs);
1419 foreach ($aPlaceIDs as $iPlaceID) {
1420 // array for placeID => -1 | Tiger housenumber
1421 $aResultPlaceIDs[$iPlaceID] = $searchedHousenumber;
1423 if ($iQueryLoop > 20) break;
1426 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30)) {
1427 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1428 // reduces the number of place ids, like a filter
1429 // rank_address is 30 for interpolated housenumbers
1430 $sSQL = "select place_id from placex where place_id in (".join(',', array_keys($aResultPlaceIDs)).") ";
1431 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1432 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1433 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',', $this->aAddressRankList).")";
1434 if (CONST_Use_US_Tiger_Data) {
1435 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',', array_keys($aResultPlaceIDs)).") ";
1436 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1437 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',', $this->aAddressRankList).")";
1439 $sSQL .= ") UNION select place_id from location_property_osmline where place_id in (".join(',', array_keys($aResultPlaceIDs)).")";
1440 $sSQL .= " and (30 between $this->iMinAddressRank and $this->iMaxAddressRank)";
1441 if (CONST_Debug) var_dump($sSQL);
1442 $aFilteredPlaceIDs = chksql($this->oDB->getCol($sSQL));
1444 foreach ($aFilteredPlaceIDs as $placeID) {
1445 $tempIDs[$placeID] = $aResultPlaceIDs[$placeID]; //assign housenumber to placeID
1447 $aResultPlaceIDs = $tempIDs;
1451 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1452 if ($iGroupLoop > 4) break;
1453 if ($iQueryLoop > 30) break;
1456 // Did we find anything?
1457 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) {
1458 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1461 // Just interpret as a reverse geocode
1462 $oReverse = new ReverseGeocode($this->oDB);
1463 $oReverse->setZoom(18);
1465 $aLookup = $oReverse->lookup(
1466 (float)$this->aNearPoint[0],
1467 (float)$this->aNearPoint[1],
1471 if (CONST_Debug) var_dump("Reverse search", $aLookup);
1473 if ($aLookup['place_id']) {
1474 $aSearchResults = $this->getDetails(array($aLookup['place_id'] => -1));
1476 $aSearchResults = array();
1481 if (!sizeof($aSearchResults)) {
1482 if ($this->bFallback) {
1483 if ($this->fallbackStructuredQuery()) {
1484 return $this->lookup();
1491 $aClassType = getClassTypesWithImportance();
1492 $aRecheckWords = preg_split('/\b[\s,\\-]*/u', $sQuery);
1493 foreach ($aRecheckWords as $i => $sWord) {
1494 if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1498 echo '<i>Recheck words:<\i>'; var_dump($aRecheckWords);
1501 $oPlaceLookup = new PlaceLookup($this->oDB);
1502 $oPlaceLookup->setIncludePolygonAsPoints($this->bIncludePolygonAsPoints);
1503 $oPlaceLookup->setIncludePolygonAsText($this->bIncludePolygonAsText);
1504 $oPlaceLookup->setIncludePolygonAsGeoJSON($this->bIncludePolygonAsGeoJSON);
1505 $oPlaceLookup->setIncludePolygonAsKML($this->bIncludePolygonAsKML);
1506 $oPlaceLookup->setIncludePolygonAsSVG($this->bIncludePolygonAsSVG);
1507 $oPlaceLookup->setPolygonSimplificationThreshold($this->fPolygonSimplificationThreshold);
1509 foreach ($aSearchResults as $iResNum => $aResult) {
1511 $fDiameter = getResultDiameter($aResult);
1513 $aOutlineResult = $oPlaceLookup->getOutlines($aResult['place_id'], $aResult['lon'], $aResult['lat'], $fDiameter/2);
1514 if ($aOutlineResult) {
1515 $aResult = array_merge($aResult, $aOutlineResult);
1518 if ($aResult['extra_place'] == 'city') {
1519 $aResult['class'] = 'place';
1520 $aResult['type'] = 'city';
1521 $aResult['rank_search'] = 16;
1524 // Is there an icon set for this type of result?
1525 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1526 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon']
1528 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1531 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1532 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label']
1534 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1535 } elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1536 && $aClassType[$aResult['class'].':'.$aResult['type']]['label']
1538 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1540 // if tag '&addressdetails=1' is set in query
1541 if ($this->bIncludeAddressDetails) {
1542 // getAddressDetails() is defined in lib.php and uses the SQL function get_addressdata in functions.sql
1543 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code'], $aResultPlaceIDs[$aResult['place_id']]);
1544 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city'])) {
1545 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1549 if ($this->bIncludeExtraTags) {
1550 if ($aResult['extra']) {
1551 $aResult['sExtraTags'] = json_decode($aResult['extra']);
1553 $aResult['sExtraTags'] = (object) array();
1557 if ($this->bIncludeNameDetails) {
1558 if ($aResult['names']) {
1559 $aResult['sNameDetails'] = json_decode($aResult['names']);
1561 $aResult['sNameDetails'] = (object) array();
1565 // Adjust importance for the number of exact string matches in the result
1566 $aResult['importance'] = max(0.001, $aResult['importance']);
1568 $sAddress = $aResult['langaddress'];
1569 foreach ($aRecheckWords as $i => $sWord) {
1570 if (stripos($sAddress, $sWord)!==false) {
1572 if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1576 $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
1578 $aResult['name'] = $aResult['langaddress'];
1579 // secondary ordering (for results with same importance (the smaller the better):
1580 // - approximate importance of address parts
1581 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1582 // - number of exact matches from the query
1583 if (isset($this->exactMatchCache[$aResult['place_id']])) {
1584 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1585 } elseif (isset($this->exactMatchCache[$aResult['parent_place_id']])) {
1586 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1588 // - importance of the class/type
1589 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1590 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance']
1592 $aResult['foundorder'] += 0.0001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1594 $aResult['foundorder'] += 0.01;
1596 if (CONST_Debug) var_dump($aResult);
1597 $aSearchResults[$iResNum] = $aResult;
1599 uasort($aSearchResults, 'byImportance');
1601 $aOSMIDDone = array();
1602 $aClassTypeNameDone = array();
1603 $aToFilter = $aSearchResults;
1604 $aSearchResults = array();
1607 foreach ($aToFilter as $iResNum => $aResult) {
1608 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1610 $fLat = $aResult['lat'];
1611 $fLon = $aResult['lon'];
1612 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1615 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1616 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']]))
1618 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1619 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1620 $aSearchResults[] = $aResult;
1623 // Absolute limit on number of results
1624 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1627 return $aSearchResults;