]> git.openstreetmap.org Git - nominatim.git/blob - lib/Geocode.php
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / lib / Geocode.php
1 <?php
2         class Geocode
3         {
4                 protected $oDB;
5
6                 protected $aLangPrefOrder = array();
7
8                 protected $bIncludeAddressDetails = false;
9
10                 protected $bIncludePolygonAsPoints = false;
11                 protected $bIncludePolygonAsText = false;
12                 protected $bIncludePolygonAsGeoJSON = false;
13                 protected $bIncludePolygonAsKML = false;
14                 protected $bIncludePolygonAsSVG = false;
15                 protected $fPolygonSimplificationThreshold = 0.0;
16
17                 protected $aExcludePlaceIDs = array();
18                 protected $bDeDupe = true;
19                 protected $bReverseInPlan = true;
20
21                 protected $iLimit = 20;
22                 protected $iFinalLimit = 10;
23                 protected $iOffset = 0;
24                 protected $bFallback = false;
25
26                 protected $aCountryCodes = false;
27                 protected $aNearPoint = false;
28
29                 protected $bBoundedSearch = false;
30                 protected $aViewBox = false;
31                 protected $sViewboxSmallSQL = false;
32                 protected $sViewboxLargeSQL = false;
33                 protected $aRoutePoints = false;
34
35                 protected $iMaxRank = 20;
36                 protected $iMinAddressRank = 0;
37                 protected $iMaxAddressRank = 30;
38                 protected $aAddressRankList = array();
39                 protected $exactMatchCache = array();
40
41                 protected $sAllowedTypesSQLList = false;
42
43                 protected $sQuery = false;
44                 protected $aStructuredQuery = false;
45
46                 function Geocode(&$oDB)
47                 {
48                         $this->oDB =& $oDB;
49                 }
50
51                 function setReverseInPlan($bReverse)
52                 {
53                         $this->bReverseInPlan = $bReverse;
54                 }
55
56                 function setLanguagePreference($aLangPref)
57                 {
58                         $this->aLangPrefOrder = $aLangPref;
59                 }
60
61                 function setIncludeAddressDetails($bAddressDetails = true)
62                 {
63                         $this->bIncludeAddressDetails = (bool)$bAddressDetails;
64                 }
65
66                 function getIncludeAddressDetails()
67                 {
68                         return $this->bIncludeAddressDetails;
69                 }
70
71                 function setIncludePolygonAsPoints($b = true)
72                 {
73                         $this->bIncludePolygonAsPoints = $b;
74                 }
75
76                 function getIncludePolygonAsPoints()
77                 {
78                         return $this->bIncludePolygonAsPoints;
79                 }
80
81                 function setIncludePolygonAsText($b = true)
82                 {
83                         $this->bIncludePolygonAsText = $b;
84                 }
85
86                 function getIncludePolygonAsText()
87                 {
88                         return $this->bIncludePolygonAsText;
89                 }
90
91                 function setIncludePolygonAsGeoJSON($b = true)
92                 {
93                         $this->bIncludePolygonAsGeoJSON = $b;
94                 }
95
96                 function setIncludePolygonAsKML($b = true)
97                 {
98                         $this->bIncludePolygonAsKML = $b;
99                 }
100
101                 function setIncludePolygonAsSVG($b = true)
102                 {
103                         $this->bIncludePolygonAsSVG = $b;
104                 }
105
106                 function setPolygonSimplificationThreshold($f)
107                 {
108                         $this->fPolygonSimplificationThreshold = $f;
109                 }
110
111                 function setDeDupe($bDeDupe = true)
112                 {
113                         $this->bDeDupe = (bool)$bDeDupe;
114                 }
115
116                 function setLimit($iLimit = 10)
117                 {
118                         if ($iLimit > 50) $iLimit = 50;
119                         if ($iLimit < 1) $iLimit = 1;
120
121                         $this->iFinalLimit = $iLimit;
122                         $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
123                 }
124
125                 function setOffset($iOffset = 0)
126                 {
127                         $this->iOffset = $iOffset;
128                 }
129
130                 function setFallback($bFallback = true)
131                 {
132                         $this->bFallback = (bool)$bFallback;
133                 }
134
135                 function setExcludedPlaceIDs($a)
136                 {
137                         // TODO: force to int
138                         $this->aExcludePlaceIDs = $a;
139                 }
140
141                 function getExcludedPlaceIDs()
142                 {
143                         return $this->aExcludePlaceIDs;
144                 }
145
146                 function setBounded($bBoundedSearch = true)
147                 {
148                         $this->bBoundedSearch = (bool)$bBoundedSearch;
149                 }
150
151                 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
152                 {
153                         $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
154                 }
155
156                 function getViewBoxString()
157                 {
158                         if (!$this->aViewBox) return null;
159                         return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
160                 }
161
162                 function setRoute($aRoutePoints)
163                 {
164                         $this->aRoutePoints = $aRoutePoints;
165                 }
166
167                 function setFeatureType($sFeatureType)
168                 {
169                         switch($sFeatureType)
170                         {
171                         case 'country':
172                                 $this->setRankRange(4, 4);
173                                 break;
174                         case 'state':
175                                 $this->setRankRange(8, 8);
176                                 break;
177                         case 'city':
178                                 $this->setRankRange(14, 16);
179                                 break;
180                         case 'settlement':
181                                 $this->setRankRange(8, 20);
182                                 break;
183                         }
184                 }
185
186                 function setRankRange($iMin, $iMax)
187                 {
188                         $this->iMinAddressRank = (int)$iMin;
189                         $this->iMaxAddressRank = (int)$iMax;
190                 }
191
192                 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
193                 {
194                         $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
195                 }
196
197                 function setCountryCodesList($aCountryCodes)
198                 {
199                         $this->aCountryCodes = $aCountryCodes;
200                 }
201
202                 function setQuery($sQueryString)
203                 {
204                         $this->sQuery = $sQueryString;
205                         $this->aStructuredQuery = false;
206                 }
207
208                 function getQueryString()
209                 {
210                         return $this->sQuery;
211                 }
212
213
214                 function loadParamArray($aParams)
215                 {
216                         if (isset($aParams['addressdetails'])) $this->bIncludeAddressDetails = (bool)$aParams['addressdetails'];
217                         if (isset($aParams['bounded'])) $this->bBoundedSearch = (bool)$aParams['bounded'];
218                         if (isset($aParams['dedupe'])) $this->bDeDupe = (bool)$aParams['dedupe'];
219
220                         if (isset($aParams['limit'])) $this->setLimit((int)$aParams['limit']);
221                         if (isset($aParams['offset'])) $this->iOffset = (int)$aParams['offset'];
222
223                         if (isset($aParams['fallback'])) $this->bFallback = (bool)$aParams['fallback'];
224
225                         // List of excluded Place IDs - used for more acurate pageing
226                         if (isset($aParams['exclude_place_ids']) && $aParams['exclude_place_ids'])
227                         {
228                                 foreach(explode(',',$aParams['exclude_place_ids']) as $iExcludedPlaceID)
229                                 {
230                                         $iExcludedPlaceID = (int)$iExcludedPlaceID;
231                                         if ($iExcludedPlaceID)
232                                                 $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
233                                 }
234
235                                 if (isset($aExcludePlaceIDs))
236                                         $this->aExcludePlaceIDs = $aExcludePlaceIDs;
237                         }
238
239                         // Only certain ranks of feature
240                         if (isset($aParams['featureType'])) $this->setFeatureType($aParams['featureType']);
241                         if (isset($aParams['featuretype'])) $this->setFeatureType($aParams['featuretype']);
242
243                         // Country code list
244                         if (isset($aParams['countrycodes']))
245                         {
246                                 $aCountryCodes = array();
247                                 foreach(explode(',',$aParams['countrycodes']) as $sCountryCode)
248                                 {
249                                         if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
250                                         {
251                                                 $aCountryCodes[] = strtolower($sCountryCode);
252                                         }
253                                 }
254                                 $this->aCountryCodes = $aCountryCodes;
255                         }
256
257                         if (isset($aParams['viewboxlbrt']) && $aParams['viewboxlbrt'])
258                         {
259                                 $aCoOrdinatesLBRT = explode(',',$aParams['viewboxlbrt']);
260                                 $this->setViewBox($aCoOrdinatesLBRT[0], $aCoOrdinatesLBRT[1], $aCoOrdinatesLBRT[2], $aCoOrdinatesLBRT[3]);
261                         }
262                         else if (isset($aParams['viewbox']) && $aParams['viewbox'])
263                         {
264                                 $aCoOrdinatesLTRB = explode(',',$aParams['viewbox']);
265                                 $this->setViewBox($aCoOrdinatesLTRB[0], $aCoOrdinatesLTRB[3], $aCoOrdinatesLTRB[2], $aCoOrdinatesLTRB[1]);
266                         }
267
268                         if (isset($aParams['route']) && $aParams['route'] && isset($aParams['routewidth']) && $aParams['routewidth'])
269                         {
270                                 $aPoints = explode(',',$aParams['route']);
271                                 if (sizeof($aPoints) % 2 != 0)
272                                 {
273                                         userError("Uneven number of points");
274                                         exit;
275                                 }
276                                 $fPrevCoord = false;
277                                 $aRoute = array();
278                                 foreach($aPoints as $i => $fPoint)
279                                 {
280                                         if ($i%2)
281                                         {
282                                                 $aRoute[] = array((float)$fPoint, $fPrevCoord);
283                                         }
284                                         else
285                                         {
286                                                 $fPrevCoord = (float)$fPoint;
287                                         }
288                                 }
289                                 $this->aRoutePoints = $aRoute;
290                         }
291                 }
292
293                 function setQueryFromParams($aParams)
294                 {
295                         // Search query
296                         $sQuery = (isset($aParams['q'])?trim($aParams['q']):'');
297                         if (!$sQuery)
298                         {
299                                 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
300                                 $this->setReverseInPlan(false);
301                         }
302                         else
303                         {
304                                 $this->setQuery($sQuery);
305                         }
306                 }
307
308                 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
309                 {
310                         $sValue = trim($sValue);
311                         if (!$sValue) return false;
312                         $this->aStructuredQuery[$sKey] = $sValue;
313                         if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
314                         {
315                                 $this->iMinAddressRank = $iNewMinAddressRank;
316                                 $this->iMaxAddressRank = $iNewMaxAddressRank;
317                         }
318                         if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
319                         return true;
320                 }
321
322                 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
323                 {
324                         $this->sQuery = false;
325
326                         // Reset
327                         $this->iMinAddressRank = 0;
328                         $this->iMaxAddressRank = 30;
329                         $this->aAddressRankList = array();
330
331                         $this->aStructuredQuery = array();
332                         $this->sAllowedTypesSQLList = '';
333
334                         $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
335                         $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
336                         $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
337                         $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
338                         $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
339                         $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
340                         $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
341
342                         if (sizeof($this->aStructuredQuery) > 0) 
343                         {
344                                 $this->sQuery = join(', ', $this->aStructuredQuery);
345                                 if ($this->iMaxAddressRank < 30)
346                                 {
347                                         $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
348                                 }
349                         }
350                 }
351
352                 function fallbackStructuredQuery()
353                 {
354                         if (!$this->aStructuredQuery) return false;
355
356                         $aParams = $this->aStructuredQuery;
357
358                         if (sizeof($aParams) == 1) return false;
359
360                         $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
361
362                         foreach($aOrderToFallback as $sType)
363                         {
364                                 if (isset($aParams[$sType]))
365                                 {
366                                         unset($aParams[$sType]);
367                                         $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
368                                         return true;
369                                 }
370                         }
371
372                         return false;
373                 }
374
375                 function getDetails($aPlaceIDs)
376                 {
377                         if (sizeof($aPlaceIDs) == 0)  return array();
378
379                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
380
381                         // Get the details for display (is this a redundant extra step?)
382                         $sPlaceIDs = join(',',$aPlaceIDs);
383
384                         $sImportanceSQL = '';
385                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
386                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
387
388                         $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,";
389                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
390                         $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
391                         $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
392                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
393                         $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
394                         $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, ";
395                         $sSQL .= "(extratags->'place') as extra_place ";
396                         $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
397                         $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
398                         if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
399                         if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
400                         $sSQL .= ") ";
401                         if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
402                         $sSQL .= "and linked_place_id is null ";
403                         $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
404                         if (!$this->bDeDupe) $sSQL .= ",place_id";
405                         $sSQL .= ",langaddress ";
406                         $sSQL .= ",placename ";
407                         $sSQL .= ",ref ";
408                         $sSQL .= ",extratags->'place' ";
409
410                         if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
411                         {
412                                 $sSQL .= " union ";
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, $sLanguagePrefArraySQL) as langaddress,";
415                                 $sSQL .= "null as placename,";
416                                 $sSQL .= "null as ref,";
417                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
418                                 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
419                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_tiger.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
420                                 $sSQL .= "null as extra_place ";
421                                 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
422                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
423                                 $sSQL .= "group by place_id";
424                                 if (!$this->bDeDupe) $sSQL .= ",place_id ";
425                                 /*
426                                 $sSQL .= " union ";
427                                 $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, min(parent_place_id) as parent_place_id,'us' as country_code,";
428                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
429                                 $sSQL .= "null as placename,";
430                                 $sSQL .= "null as ref,";
431                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
432                                 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
433                                 $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, ";
434                                 $sSQL .= "null as extra_place ";
435                                 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
436                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
437                                 $sSQL .= "group by place_id";
438                                 if (!$this->bDeDupe) $sSQL .= ",place_id";
439                                 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
440                                 */
441                         }
442
443                         $sSQL .= " order by importance desc";
444                         if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
445                         $aSearchResults = $this->oDB->getAll($sSQL);
446
447                         if (PEAR::IsError($aSearchResults))
448                         {
449                                 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
450                         }
451
452                         return $aSearchResults;
453                 }
454
455                 function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
456                 {
457                         /*
458                            Calculate all searches using aValidTokens i.e.
459                            'Wodsworth Road, Sheffield' =>
460
461                            Phrase Wordset
462                            0      0       (wodsworth road)
463                            0      1       (wodsworth)(road)
464                            1      0       (sheffield)
465
466                            Score how good the search is so they can be ordered
467                          */
468                         foreach($aPhrases as $iPhrase => $sPhrase)
469                         {
470                                 $aNewPhraseSearches = array();
471                                 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
472                                 else $sPhraseType = '';
473
474                                 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
475                                 {
476                                         // Too many permutations - too expensive
477                                         if ($iWordSet > 120) break;
478
479                                         $aWordsetSearches = $aSearches;
480
481                                         // Add all words from this wordset
482                                         foreach($aWordset as $iToken => $sToken)
483                                         {
484                                                 //echo "<br><b>$sToken</b>";
485                                                 $aNewWordsetSearches = array();
486
487                                                 foreach($aWordsetSearches as $aCurrentSearch)
488                                                 {
489                                                         //echo "<i>";
490                                                         //var_dump($aCurrentSearch);
491                                                         //echo "</i>";
492
493                                                         // If the token is valid
494                                                         if (isset($aValidTokens[' '.$sToken]))
495                                                         {
496                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
497                                                                 {
498                                                                         $aSearch = $aCurrentSearch;
499                                                                         $aSearch['iSearchRank']++;
500                                                                         if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
501                                                                         {
502                                                                                 if ($aSearch['sCountryCode'] === false)
503                                                                                 {
504                                                                                         $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
505                                                                                         // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
506                                                                                         if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)))
507                                                                                         {
508                                                                                                 $aSearch['iSearchRank'] += 5;
509                                                                                         }
510                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
511                                                                                 }
512                                                                         }
513                                                                         elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
514                                                                         {
515                                                                                 if ($aSearch['fLat'] === '')
516                                                                                 {
517                                                                                         $aSearch['fLat'] = $aSearchTerm['lat'];
518                                                                                         $aSearch['fLon'] = $aSearchTerm['lon'];
519                                                                                         $aSearch['fRadius'] = $aSearchTerm['radius'];
520                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
521                                                                                 }
522                                                                         }
523                                                                         elseif ($sPhraseType == 'postalcode')
524                                                                         {
525                                                                                 // 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
526                                                                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
527                                                                                 {
528                                                                                         // If we already have a name try putting the postcode first
529                                                                                         if (sizeof($aSearch['aName']))
530                                                                                         {
531                                                                                                 $aNewSearch = $aSearch;
532                                                                                                 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
533                                                                                                 $aNewSearch['aName'] = array();
534                                                                                                 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
535                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
536                                                                                         }
537
538                                                                                         if (sizeof($aSearch['aName']))
539                                                                                         {
540                                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
541                                                                                                 {
542                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
543                                                                                                 }
544                                                                                                 else
545                                                                                                 {
546                                                                                                         $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
547                                                                                                         $aSearch['iSearchRank'] += 1000; // skip;
548                                                                                                 }
549                                                                                         }
550                                                                                         else
551                                                                                         {
552                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
553                                                                                                 //$aSearch['iNamePhrase'] = $iPhrase;
554                                                                                         }
555                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
556                                                                                 }
557
558                                                                         }
559                                                                         elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
560                                                                         {
561                                                                                 if ($aSearch['sHouseNumber'] === '')
562                                                                                 {
563                                                                                         $aSearch['sHouseNumber'] = $sToken;
564                                                                                         // sanity check: if the housenumber is not mainly made
565                                                                                         // up of numbers, add a penalty
566                                                                                         if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
567                                                                                         // also housenumbers should appear in the first or second phrase
568                                                                                         if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
569                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
570                                                                                         /*
571                                                                                         // Fall back to not searching for this item (better than nothing)
572                                                                                         $aSearch = $aCurrentSearch;
573                                                                                         $aSearch['iSearchRank'] += 1;
574                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
575                                                                                          */
576                                                                                 }
577                                                                         }
578                                                                         elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
579                                                                         {
580                                                                                 if ($aSearch['sClass'] === '')
581                                                                                 {
582                                                                                         $aSearch['sOperator'] = $aSearchTerm['operator'];
583                                                                                         $aSearch['sClass'] = $aSearchTerm['class'];
584                                                                                         $aSearch['sType'] = $aSearchTerm['type'];
585                                                                                         if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
586                                                                                         else $aSearch['sOperator'] = 'near'; // near = in for the moment
587                                                                                         if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
588
589                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
590                                                                                 }
591                                                                         }
592                                                                         elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
593                                                                         {
594                                                                                 if (sizeof($aSearch['aName']))
595                                                                                 {
596                                                                                         if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
597                                                                                         {
598                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
599                                                                                         }
600                                                                                         else
601                                                                                         {
602                                                                                                 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
603                                                                                                 $aSearch['iSearchRank'] += 1000; // skip;
604                                                                                         }
605                                                                                 }
606                                                                                 else
607                                                                                 {
608                                                                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
609                                                                                         //$aSearch['iNamePhrase'] = $iPhrase;
610                                                                                 }
611                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
612                                                                         }
613                                                                 }
614                                                         }
615                                                         // Look for partial matches.
616                                                         // Note that there is no point in adding country terms here
617                                                         // because country are omitted in the address.
618                                                         if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country')
619                                                         {
620                                                                 // Allow searching for a word - but at extra cost
621                                                                 foreach($aValidTokens[$sToken] as $aSearchTerm)
622                                                                 {
623                                                                         if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
624                                                                         {
625                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false)
626                                                                                 {
627                                                                                         $aSearch = $aCurrentSearch;
628                                                                                         $aSearch['iSearchRank'] += 1;
629                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
630                                                                                         {
631                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
632                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
633                                                                                         }
634                                                                                         elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
635                                                                                         {
636                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
637                                                                                                 $aSearch['iSearchRank'] += 1;
638                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
639                                                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
640                                                                                                 {
641                                                                                                         if (empty($aSearchTermToken['country_code'])
642                                                                                                                         && empty($aSearchTermToken['lat'])
643                                                                                                                         && empty($aSearchTermToken['class']))
644                                                                                                         {
645                                                                                                                 $aSearch = $aCurrentSearch;
646                                                                                                                 $aSearch['iSearchRank'] += 1;
647                                                                                                                 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
648                                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
649                                                                                                         }
650                                                                                                 }
651                                                                                         }
652                                                                                         else
653                                                                                         {
654                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
655                                                                                                 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
656                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
657                                                                                         }
658                                                                                 }
659
660                                                                                 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
661                                                                                 {
662                                                                                         $aSearch = $aCurrentSearch;
663                                                                                         $aSearch['iSearchRank'] += 1;
664                                                                                         if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
665                                                                                         if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
666                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
667                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
668                                                                                         else
669                                                                                                 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
670                                                                                         $aSearch['iNamePhrase'] = $iPhrase;
671                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
672                                                                                 }
673                                                                         }
674                                                                 }
675                                                         }
676                                                         else
677                                                         {
678                                                                 // Allow skipping a word - but at EXTREAM cost
679                                                                 //$aSearch = $aCurrentSearch;
680                                                                 //$aSearch['iSearchRank']+=100;
681                                                                 //$aNewWordsetSearches[] = $aSearch;
682                                                         }
683                                                 }
684                                                 // Sort and cut
685                                                 usort($aNewWordsetSearches, 'bySearchRank');
686                                                 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
687                                         }
688                                         //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
689
690                                         $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
691                                         usort($aNewPhraseSearches, 'bySearchRank');
692
693                                         $aSearchHash = array();
694                                         foreach($aNewPhraseSearches as $iSearch => $aSearch)
695                                         {
696                                                 $sHash = serialize($aSearch);
697                                                 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
698                                                 else $aSearchHash[$sHash] = 1;
699                                         }
700
701                                         $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
702                                 }
703
704                                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
705                                 $aGroupedSearches = array();
706                                 foreach($aNewPhraseSearches as $aSearch)
707                                 {
708                                         if ($aSearch['iSearchRank'] < $this->iMaxRank)
709                                         {
710                                                 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
711                                                 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
712                                         }
713                                 }
714                                 ksort($aGroupedSearches);
715
716                                 $iSearchCount = 0;
717                                 $aSearches = array();
718                                 foreach($aGroupedSearches as $iScore => $aNewSearches)
719                                 {
720                                         $iSearchCount += sizeof($aNewSearches);
721                                         $aSearches = array_merge($aSearches, $aNewSearches);
722                                         if ($iSearchCount > 50) break;
723                                 }
724
725                                 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
726
727                         }
728                         return $aGroupedSearches;
729
730                 }
731
732                 /* Perform the actual query lookup.
733
734                         Returns an ordered list of results, each with the following fields:
735                           osm_type: type of corresponding OSM object
736                                                         N - node
737                                                         W - way
738                                                         R - relation
739                                                         P - postcode (internally computed)
740                           osm_id: id of corresponding OSM object
741                           class: general object class (corresponds to tag key of primary OSM tag)
742                           type: subclass of object (corresponds to tag value of primary OSM tag)
743                           admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
744                           rank_search: rank in search hierarchy
745                                                         (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
746                           rank_address: rank in address hierarchy (determines orer in address)
747                           place_id: internal key (may differ between different instances)
748                           country_code: ISO country code
749                           langaddress: localized full address
750                           placename: localized name of object
751                           ref: content of ref tag (if available)
752                           lon: longitude
753                           lat: latitude
754                           importance: importance of place based on Wikipedia link count
755                           addressimportance: cumulated importance of address elements
756                           extra_place: type of place (for admin boundaries, if there is a place tag)
757                           aBoundingBox: bounding Box
758                           label: short description of the object class/type (English only) 
759                           name: full name (currently the same as langaddress)
760                           foundorder: secondary ordering for places with same importance
761                 */
762                 function lookup()
763                 {
764                         if (!$this->sQuery && !$this->aStructuredQuery) return false;
765
766                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
767                         $sCountryCodesSQL = false;
768                         if ($this->aCountryCodes && sizeof($this->aCountryCodes))
769                         {
770                                 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
771                         }
772
773                         $sQuery = $this->sQuery;
774
775                         // Conflicts between US state abreviations and various words for 'the' in different languages
776                         if (isset($this->aLangPrefOrder['name:en']))
777                         {
778                                 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/','\1illinois\2', $sQuery);
779                                 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/','\1alabama\2', $sQuery);
780                                 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/','\1louisiana\2', $sQuery);
781                         }
782
783                         // View Box SQL
784                         $sViewboxCentreSQL = false;
785                         $bBoundingBoxSearch = false;
786                         if ($this->aViewBox)
787                         {
788                                 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
789                                 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
790                                 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
791                                 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
792                                 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
793                                 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
794
795                                 $this->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)";
796                                 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
797                                 $bBoundingBoxSearch = $this->bBoundedSearch;
798                         }
799
800                         // Route SQL
801                         if ($this->aRoutePoints)
802                         {
803                                 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
804                                 $bFirst = true;
805                                 foreach($this->aRoutePoints as $aPoint)
806                                 {
807                                         if (!$bFirst) $sViewboxCentreSQL .= ",";
808                                         $sViewboxCentreSQL .= $aPoint[0].' '.$aPoint[1];
809                                         $bFirst = false;
810                                 }
811                                 $sViewboxCentreSQL .= ")'::geometry,4326)";
812
813                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
814                                 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
815                                 if (PEAR::isError($this->sViewboxSmallSQL))
816                                 {
817                                         failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
818                                 }
819                                 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
820
821                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
822                                 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
823                                 if (PEAR::isError($this->sViewboxLargeSQL))
824                                 {
825                                         failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
826                                 }
827                                 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
828                                 $bBoundingBoxSearch = $this->bBoundedSearch;
829                         }
830
831                         // Do we have anything that looks like a lat/lon pair?
832                         if ( $aLooksLike = looksLikeLatLonPair($sQuery) ){
833                                 $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
834                                 $sQuery = $aLooksLike['query'];                 
835                         }
836
837                         $aSearchResults = array();
838                         if ($sQuery || $this->aStructuredQuery)
839                         {
840                                 // Start with a blank search
841                                 $aSearches = array(
842                                         array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
843                                               'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
844                                               'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
845                                 );
846
847                                 // Do we have a radius search?
848                                 $sNearPointSQL = false;
849                                 if ($this->aNearPoint)
850                                 {
851                                         $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
852                                         $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
853                                         $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
854                                         $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
855                                 }
856
857                                 // Any 'special' terms in the search?
858                                 $bSpecialTerms = false;
859                                 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
860                                 $aSpecialTerms = array();
861                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
862                                 {
863                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
864                                         $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
865                                 }
866
867                                 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
868                                 $aSpecialTerms = array();
869                                 if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity'])
870                                 {
871                                         $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
872                                         unset($this->aStructuredQuery['amenity']);
873                                 }
874                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
875                                 {
876                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
877                                         $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
878                                         $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
879                                         $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';
880                                         if (CONST_Debug) var_Dump($sSQL);
881                                         $aSearchWords = $this->oDB->getAll($sSQL);
882                                         $aNewSearches = array();
883                                         foreach($aSearches as $aSearch)
884                                         {
885                                                 foreach($aSearchWords as $aSearchTerm)
886                                                 {
887                                                         $aNewSearch = $aSearch;
888                                                         if ($aSearchTerm['country_code'])
889                                                         {
890                                                                 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
891                                                                 $aNewSearches[] = $aNewSearch;
892                                                                 $bSpecialTerms = true;
893                                                         }
894                                                         if ($aSearchTerm['class'])
895                                                         {
896                                                                 $aNewSearch['sClass'] = $aSearchTerm['class'];
897                                                                 $aNewSearch['sType'] = $aSearchTerm['type'];
898                                                                 $aNewSearches[] = $aNewSearch;
899                                                                 $bSpecialTerms = true;
900                                                         }
901                                                 }
902                                         }
903                                         $aSearches = $aNewSearches;
904                                 }
905
906                                 // Split query into phrases
907                                 // Commas are used to reduce the search space by indicating where phrases split
908                                 if ($this->aStructuredQuery)
909                                 {
910                                         $aPhrases = $this->aStructuredQuery;
911                                         $bStructuredPhrases = true;
912                                 }
913                                 else
914                                 {
915                                         $aPhrases = explode(',',$sQuery);
916                                         $bStructuredPhrases = false;
917                                 }
918
919                                 // Convert each phrase to standard form
920                                 // Create a list of standard words
921                                 // Get all 'sets' of words
922                                 // Generate a complete list of all
923                                 $aTokens = array();
924                                 foreach($aPhrases as $iPhrase => $sPhrase)
925                                 {
926                                         $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
927                                         if (PEAR::isError($aPhrase))
928                                         {
929                                                 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
930                                                 if (CONST_Debug) var_dump($aPhrase);
931                                                 exit;
932                                         }
933                                         if (trim($aPhrase['string']))
934                                         {
935                                                 $aPhrases[$iPhrase] = $aPhrase;
936                                                 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
937                                                 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
938                                                 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
939                                         }
940                                         else
941                                         {
942                                                 unset($aPhrases[$iPhrase]);
943                                         }
944                                 }
945
946                                 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
947                                 $aPhraseTypes = array_keys($aPhrases);
948                                 $aPhrases = array_values($aPhrases);
949
950                                 if (sizeof($aTokens))
951                                 {
952                                         // Check which tokens we have, get the ID numbers
953                                         $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
954                                         $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
955
956                                         if (CONST_Debug) var_Dump($sSQL);
957
958                                         $aValidTokens = array();
959                                         if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
960                                         else $aDatabaseWords = array();
961                                         if (PEAR::IsError($aDatabaseWords))
962                                         {
963                                                 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
964                                         }
965                                         $aPossibleMainWordIDs = array();
966                                         $aWordFrequencyScores = array();
967                                         foreach($aDatabaseWords as $aToken)
968                                         {
969                                                 // Very special case - require 2 letter country param to match the country code found
970                                                 if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
971                                                                 && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code'])
972                                                 {
973                                                         continue;
974                                                 }
975
976                                                 if (isset($aValidTokens[$aToken['word_token']]))
977                                                 {
978                                                         $aValidTokens[$aToken['word_token']][] = $aToken;
979                                                 }
980                                                 else
981                                                 {
982                                                         $aValidTokens[$aToken['word_token']] = array($aToken);
983                                                 }
984                                                 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
985                                                 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
986                                         }
987                                         if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
988
989                                         // Try and calculate GB postcodes we might be missing
990                                         foreach($aTokens as $sToken)
991                                         {
992                                                 // Source of gb postcodes is now definitive - always use
993                                                 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
994                                                 {
995                                                         if (substr($aData[1],-2,1) != ' ')
996                                                         {
997                                                                 $aData[0] = substr($aData[0],0,strlen($aData[1])-1).' '.substr($aData[0],strlen($aData[1])-1);
998                                                                 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
999                                                         }
1000                                                         $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
1001                                                         if ($aGBPostcodeLocation)
1002                                                         {
1003                                                                 $aValidTokens[$sToken] = $aGBPostcodeLocation;
1004                                                         }
1005                                                 }
1006                                                 // US ZIP+4 codes - if there is no token,
1007                                                 //      merge in the 5-digit ZIP code
1008                                                 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
1009                                                 {
1010                                                         if (isset($aValidTokens[$aData[1]]))
1011                                                         {
1012                                                                 foreach($aValidTokens[$aData[1]] as $aToken)
1013                                                                 {
1014                                                                         if (!$aToken['class'])
1015                                                                         {
1016                                                                                 if (isset($aValidTokens[$sToken]))
1017                                                                                 {
1018                                                                                         $aValidTokens[$sToken][] = $aToken;
1019                                                                                 }
1020                                                                                 else
1021                                                                                 {
1022                                                                                         $aValidTokens[$sToken] = array($aToken);
1023                                                                                 }
1024                                                                         }
1025                                                                 }
1026                                                         }
1027                                                 }
1028                                         }
1029
1030                                         foreach($aTokens as $sToken)
1031                                         {
1032                                                 // Unknown single word token with a number - assume it is a house number
1033                                                 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
1034                                                 {
1035                                                         $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
1036                                                 }
1037                                         }
1038
1039                                         // Any words that have failed completely?
1040                                         // TODO: suggestions
1041
1042                                         // Start the search process
1043                                         $aResultPlaceIDs = array();
1044
1045                                         $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1046
1047                                         if ($this->bReverseInPlan)
1048                                         {
1049                                                 // Reverse phrase array and also reverse the order of the wordsets in
1050                                                 // the first and final phrase. Don't bother about phrases in the middle
1051                                                 // because order in the address doesn't matter.
1052                                                 $aPhrases = array_reverse($aPhrases);
1053                                                 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1054                                                 if (sizeof($aPhrases) > 1)
1055                                                 {
1056                                                         $aFinalPhrase = end($aPhrases);
1057                                                         $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1058                                                 }
1059                                                 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1060
1061                                                 foreach($aGroupedSearches as $aSearches)
1062                                                 {
1063                                                         foreach($aSearches as $aSearch)
1064                                                         {
1065                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1066                                                                 {
1067                                                                         if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1068                                                                         $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1069                                                                 }
1070
1071                                                         }
1072                                                 }
1073
1074                                                 $aGroupedSearches = $aReverseGroupedSearches;
1075                                                 ksort($aGroupedSearches);
1076                                         }
1077                                 }
1078                                 else
1079                                 {
1080                                         // Re-group the searches by their score, junk anything over 20 as just not worth trying
1081                                         $aGroupedSearches = array();
1082                                         foreach($aSearches as $aSearch)
1083                                         {
1084                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1085                                                 {
1086                                                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1087                                                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1088                                                 }
1089                                         }
1090                                         ksort($aGroupedSearches);
1091                                 }
1092
1093                                 if (CONST_Debug) var_Dump($aGroupedSearches);
1094
1095                                 if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0)
1096                                 {
1097                                         $aCopyGroupedSearches = $aGroupedSearches;
1098                                         foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1099                                         {
1100                                                 foreach($aSearches as $iSearch => $aSearch)
1101                                                 {
1102                                                         $aReductionsList = array($aSearch['aAddress']);
1103                                                         $iSearchRank = $aSearch['iSearchRank'];
1104                                                         while(sizeof($aReductionsList) > 0)
1105                                                         {
1106                                                                 $iSearchRank += 5;
1107                                                                 if ($iSearchRank > iMaxRank) break 3;
1108                                                                 $aNewReductionsList = array();
1109                                                                 foreach($aReductionsList as $aReductionsWordList)
1110                                                                 {
1111                                                                         for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1112                                                                         {
1113                                                                                 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1114                                                                                 $aReverseSearch = $aSearch;
1115                                                                                 $aSearch['aAddress'] = $aReductionsWordListResult;
1116                                                                                 $aSearch['iSearchRank'] = $iSearchRank;
1117                                                                                 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1118                                                                                 if (sizeof($aReductionsWordListResult) > 0)
1119                                                                                 {
1120                                                                                         $aNewReductionsList[] = $aReductionsWordListResult;
1121                                                                                 }
1122                                                                         }
1123                                                                 }
1124                                                                 $aReductionsList = $aNewReductionsList;
1125                                                         }
1126                                                 }
1127                                         }
1128                                         ksort($aGroupedSearches);
1129                                 }
1130
1131                                 // Filter out duplicate searches
1132                                 $aSearchHash = array();
1133                                 foreach($aGroupedSearches as $iGroup => $aSearches)
1134                                 {
1135                                         foreach($aSearches as $iSearch => $aSearch)
1136                                         {
1137                                                 $sHash = serialize($aSearch);
1138                                                 if (isset($aSearchHash[$sHash]))
1139                                                 {
1140                                                         unset($aGroupedSearches[$iGroup][$iSearch]);
1141                                                         if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1142                                                 }
1143                                                 else
1144                                                 {
1145                                                         $aSearchHash[$sHash] = 1;
1146                                                 }
1147                                         }
1148                                 }
1149
1150                                 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1151
1152                                 $iGroupLoop = 0;
1153                                 $iQueryLoop = 0;
1154                                 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1155                                 {
1156                                         $iGroupLoop++;
1157                                         foreach($aSearches as $aSearch)
1158                                         {
1159                                                 $iQueryLoop++;
1160
1161                                                 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1162                                                 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1163
1164                                                 // No location term?
1165                                                 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1166                                                 {
1167                                                         if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1168                                                         {
1169                                                                 // Just looking for a country by code - look it up
1170                                                                 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1171                                                                 {
1172                                                                         $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1173                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1174                                     if ($bBoundingBoxSearch)
1175                                         $sSQL .= " and _st_intersects($this->sViewboxSmallSQL, geometry)";
1176                                                                         $sSQL .= " order by st_area(geometry) desc limit 1";
1177                                                                         if (CONST_Debug) var_dump($sSQL);
1178                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1179                                                                 }
1180                                                                 else
1181                                                                 {
1182                                                                         $aPlaceIDs = array();
1183                                                                 }
1184                                                         }
1185                                                         else
1186                                                         {
1187                                                                 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1188                                                                 if (!$aSearch['sClass']) continue;
1189                                                                 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1190                                                                 if ($this->oDB->getOne($sSQL))
1191                                                                 {
1192                                                                         $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1193                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1194                                                                         $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1195                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1196                                                                         if (sizeof($this->aExcludePlaceIDs))
1197                                                                         {
1198                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1199                                                                         }
1200                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1201                                                                         $sSQL .= " limit $this->iLimit";
1202                                                                         if (CONST_Debug) var_dump($sSQL);
1203                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1204
1205                                                                         // If excluded place IDs are given, it is fair to assume that
1206                                                                         // there have been results in the small box, so no further
1207                                                                         // expansion in that case.
1208                                                                         // Also don't expand if bounded results were requested.
1209                                                                         if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch)
1210                                                                         {
1211                                                                                 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1212                                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1213                                                                                 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1214                                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1215                                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1216                                                                                 $sSQL .= " limit $this->iLimit";
1217                                                                                 if (CONST_Debug) var_dump($sSQL);
1218                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1219                                                                         }
1220                                                                 }
1221                                                                 else
1222                                                                 {
1223                                                                         $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1224                                                                         $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1225                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1226                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1227                                                                         $sSQL .= " limit $this->iLimit";
1228                                                                         if (CONST_Debug) var_dump($sSQL);
1229                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1230                                                                 }
1231                                                         }
1232                                                 }
1233                                                 else
1234                                                 {
1235                                                         $aPlaceIDs = array();
1236
1237                                                         // First we need a position, either aName or fLat or both
1238                                                         $aTerms = array();
1239                                                         $aOrder = array();
1240
1241                                                         if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress']))
1242                                                         {
1243                                                                 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1244                                                                 $aOrder[] = "exists(select place_id from placex where parent_place_id = search_name.place_id and transliteration(housenumber) ~* E'".$sHouseNumberRegex."' limit 1) desc";
1245                                                         }
1246
1247                                                         // TODO: filter out the pointless search terms (2 letter name tokens and less)
1248                                                         // they might be right - but they are just too darned expensive to run
1249                                                         if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1250                                                         //if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1251                                                         if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1252                                                         {
1253                                                                 // For infrequent name terms disable index usage for address
1254                                                                 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1255                                                                                 sizeof($aSearch['aName']) == 1 &&
1256                                                                                 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1257                                                                 {
1258                                                                         //$aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1259                                                                         $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddress'],",")."]";
1260                                                                 }
1261                                                                 else
1262                                                                 {
1263                                                                         $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1264                                                                         //if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1265                                                                 }
1266                                                         }
1267                                                         if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1268                                                         if ($aSearch['sHouseNumber'])
1269                                                         {
1270                                                                 $aTerms[] = "address_rank between 16 and 27";
1271                                                         }
1272                                                         else
1273                                                         {
1274                                                                 if ($this->iMinAddressRank > 0)
1275                                                                 {
1276                                                                         $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1277                                                                 }
1278                                                                 if ($this->iMaxAddressRank < 30)
1279                                                                 {
1280                                                                         $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1281                                                                 }
1282                                                         }
1283                                                         if ($aSearch['fLon'] && $aSearch['fLat'])
1284                                                         {
1285                                                                 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1286                                                                 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1287                                                         }
1288                                                         if (sizeof($this->aExcludePlaceIDs))
1289                                                         {
1290                                                                 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1291                                                         }
1292                                                         if ($sCountryCodesSQL)
1293                                                         {
1294                                                                 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1295                                                         }
1296
1297                                                         if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1298                                                         if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1299
1300                                                         if ($aSearch['sHouseNumber'])
1301                                                         {
1302                                                                 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1303                                                         }
1304                                                         else
1305                                                         {
1306                                                                 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1307                                                         }
1308                                                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1309                                                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1310
1311                                                         $aOrder[] = "$sImportanceSQL DESC";
1312                                                         if (sizeof($aSearch['aFullNameAddress']))
1313                                                         {
1314                                                                 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1315                                                                 $aOrder[] = 'exactmatch DESC';
1316                                                         } else {
1317                                                                 $sExactMatchSQL = '0::int as exactmatch';
1318                                                         }
1319
1320                                                         if (sizeof($aTerms))
1321                                                         {
1322                                                                 $sSQL = "select place_id, ";
1323                                                                 $sSQL .= $sExactMatchSQL;
1324                                                                 $sSQL .= " from search_name";
1325                                                                 $sSQL .= " where ".join(' and ',$aTerms);
1326                                                                 $sSQL .= " order by ".join(', ',$aOrder);
1327                                                                 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1328                                                                         $sSQL .= " limit 20";
1329                                                                 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1330                                                                         $sSQL .= " limit 1";
1331                                                                 else
1332                                                                         $sSQL .= " limit ".$this->iLimit;
1333
1334                                                                 if (CONST_Debug) { var_dump($sSQL); }
1335                                                                 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1336                                                                 if (PEAR::IsError($aViewBoxPlaceIDs))
1337                                                                 {
1338                                                                         failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1339                                                                 }
1340                                                                 //var_dump($aViewBoxPlaceIDs);
1341                                                                 // Did we have an viewbox matches?
1342                                                                 $aPlaceIDs = array();
1343                                                                 $bViewBoxMatch = false;
1344                                                                 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1345                                                                 {
1346                                                                         //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1347                                                                         //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1348                                                                         //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1349                                                                         //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1350                                                                         $aPlaceIDs[] = $aViewBoxRow['place_id'];
1351                                                                         $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1352                                                                 }
1353                                                         }
1354                                                         //var_Dump($aPlaceIDs);
1355                                                         //exit;
1356
1357                                                         if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1358                                                         {
1359                                                                 $aRoadPlaceIDs = $aPlaceIDs;
1360                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1361
1362                                                                 // Now they are indexed look for a house attached to a street we found
1363                                                                 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1364                                                                 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1365                                                                 if (sizeof($this->aExcludePlaceIDs))
1366                                                                 {
1367                                                                         $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1368                                                                 }
1369                                                                 $sSQL .= " limit $this->iLimit";
1370                                                                 if (CONST_Debug) var_dump($sSQL);
1371                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1372
1373                                                                 // If not try the aux fallback table
1374                                                                 /*
1375                                                                 if (!sizeof($aPlaceIDs))
1376                                                                 {
1377                                                                         $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1378                                                                         if (sizeof($this->aExcludePlaceIDs))
1379                                                                         {
1380                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1381                                                                         }
1382                                                                         //$sSQL .= " limit $this->iLimit";
1383                                                                         if (CONST_Debug) var_dump($sSQL);
1384                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1385                                                                 }
1386                                                                 */
1387
1388                                                                 if (!sizeof($aPlaceIDs))
1389                                                                 {
1390                                                                         $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1391                                                                         if (sizeof($this->aExcludePlaceIDs))
1392                                                                         {
1393                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1394                                                                         }
1395                                                                         //$sSQL .= " limit $this->iLimit";
1396                                                                         if (CONST_Debug) var_dump($sSQL);
1397                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1398                                                                 }
1399
1400                                                                 // Fallback to the road
1401                                                                 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1402                                                                 {
1403                                                                         $aPlaceIDs = $aRoadPlaceIDs;
1404                                                                 }
1405
1406                                                         }
1407
1408                                                         if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1409                                                         {
1410                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1411                                                                 $aClassPlaceIDs = array();
1412
1413                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1414                                                                 {
1415                                                                         // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1416                                                                         $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1417                                                                         $sSQL .= " and linked_place_id is null";
1418                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1419                                                                         $sSQL .= " order by rank_search asc limit $this->iLimit";
1420                                                                         if (CONST_Debug) var_dump($sSQL);
1421                                                                         $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1422                                                                 }
1423
1424                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1425                                                                 {
1426                                                                         $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1427                                                                         $bCacheTable = $this->oDB->getOne($sSQL);
1428
1429                                                                         $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1430
1431                                                                         if (CONST_Debug) var_dump($sSQL);
1432                                                                         $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1433
1434                                                                         // For state / country level searches the normal radius search doesn't work very well
1435                                                                         $sPlaceGeom = false;
1436                                                                         if ($this->iMaxRank < 9 && $bCacheTable)
1437                                                                         {
1438                                                                                 // Try and get a polygon to search in instead
1439                                                                                 $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";
1440                                                                                 if (CONST_Debug) var_dump($sSQL);
1441                                                                                 $sPlaceGeom = $this->oDB->getOne($sSQL);
1442                                                                         }
1443
1444                                                                         if ($sPlaceGeom)
1445                                                                         {
1446                                                                                 $sPlaceIDs = false;
1447                                                                         }
1448                                                                         else
1449                                                                         {
1450                                                                                 $this->iMaxRank += 5;
1451                                                                                 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1452                                                                                 if (CONST_Debug) var_dump($sSQL);
1453                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1454                                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1455                                                                         }
1456
1457                                                                         if ($sPlaceIDs || $sPlaceGeom)
1458                                                                         {
1459
1460                                                                                 $fRange = 0.01;
1461                                                                                 if ($bCacheTable)
1462                                                                                 {
1463                                                                                         // More efficient - can make the range bigger
1464                                                                                         $fRange = 0.05;
1465
1466                                                                                         $sOrderBySQL = '';
1467                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1468                                                                                         else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1469                                                                                         else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1470
1471                                                                                         $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1472                                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1473                                                                                         if ($sPlaceIDs)
1474                                                                                         {
1475                                                                                                 $sSQL .= ",placex as f where ";
1476                                                                                                 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1477                                                                                         }
1478                                                                                         if ($sPlaceGeom)
1479                                                                                         {
1480                                                                                                 $sSQL .= " where ";
1481                                                                                                 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1482                                                                                         }
1483                                                                                         if (sizeof($this->aExcludePlaceIDs))
1484                                                                                         {
1485                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1486                                                                                         }
1487                                                                                         if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1488                                                                                         if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1489                                                                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1490                                                                                         $sSQL .= " limit $this->iLimit";
1491                                                                                         if (CONST_Debug) var_dump($sSQL);
1492                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1493                                                                                 }
1494                                                                                 else
1495                                                                                 {
1496                                                                                         if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1497
1498                                                                                         $sOrderBySQL = '';
1499                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1500                                                                                         else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1501
1502                                                                                         $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1503                                                                                         $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1504                                                                                         $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1505                                                                                         if (sizeof($this->aExcludePlaceIDs))
1506                                                                                         {
1507                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1508                                                                                         }
1509                                                                                         if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1510                                                                                         if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1511                                                                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1512                                                                                         $sSQL .= " limit $this->iLimit";
1513                                                                                         if (CONST_Debug) var_dump($sSQL);
1514                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1515                                                                                 }
1516                                                                         }
1517                                                                 }
1518
1519                                                                 $aPlaceIDs = $aClassPlaceIDs;
1520
1521                                                         }
1522
1523                                                 }
1524
1525                                                 if (PEAR::IsError($aPlaceIDs))
1526                                                 {
1527                                                         failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1528                                                 }
1529
1530                                                 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1531
1532                                                 foreach($aPlaceIDs as $iPlaceID)
1533                                                 {
1534                                                         $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1535                                                 }
1536                                                 if ($iQueryLoop > 20) break;
1537                                         }
1538
1539                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1540                                         {
1541                                                 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1542                                                 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1543                                                 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1544                                                 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1545                                                 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1546                                                 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1547                                                 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1548                                                 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1549                                                 $sSQL .= ")";
1550                                                 if (CONST_Debug) var_dump($sSQL);
1551                                                 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1552                                         }
1553
1554                                         //exit;
1555                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1556                                         if ($iGroupLoop > 4) break;
1557                                         if ($iQueryLoop > 30) break;
1558                                 }
1559
1560                                 // Did we find anything?
1561                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1562                                 {
1563                                         $aSearchResults = $this->getDetails($aResultPlaceIDs);
1564                                 }
1565
1566                         }
1567                         else
1568                         {
1569                                 // Just interpret as a reverse geocode
1570                                 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1571                                 if ($iPlaceID)
1572                                         $aSearchResults = $this->getDetails(array($iPlaceID));
1573                                 else
1574                                         $aSearchResults = array();
1575                         }
1576
1577                         // No results? Done
1578                         if (!sizeof($aSearchResults))
1579                         {
1580                                 if ($this->bFallback)
1581                                 {
1582                                         if ($this->fallbackStructuredQuery())
1583                                         {
1584                                                 return $this->lookup();
1585                                         }
1586                                 }
1587
1588                                 return array();
1589                         }
1590
1591                         $aClassType = getClassTypesWithImportance();
1592                         $aRecheckWords = preg_split('/\b[\s,\\-]*/u',$sQuery);
1593                         foreach($aRecheckWords as $i => $sWord)
1594                         {
1595                                 if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1596                         }
1597
1598             if (CONST_Debug) { echo '<i>Recheck words:<\i>'; var_dump($aRecheckWords); }
1599
1600                         foreach($aSearchResults as $iResNum => $aResult)
1601                         {
1602                                 if (CONST_Search_AreaPolygons)
1603                                 {
1604                                         // Get the bounding box and outline polygon
1605                                         $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1606                                         $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1607                                         $sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,";
1608                                         $sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon";
1609                                         if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1610                                         if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1611                                         if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1612                                         if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1613                                         $sFrom = " from placex where place_id = ".$aResult['place_id'];
1614                                         if ($this->fPolygonSimplificationThreshold > 0)
1615                                         {
1616                                                 $sSQL .= " from (select place_id,centroid,ST_SimplifyPreserveTopology(geometry,".$this->fPolygonSimplificationThreshold.") as geometry".$sFrom.") as plx";
1617                                         }
1618                                         else
1619                                         {
1620                                                 $sSQL .= $sFrom;
1621                                         }
1622
1623                                         $aPointPolygon = $this->oDB->getRow($sSQL);
1624                                         if (PEAR::IsError($aPointPolygon))
1625                                         {
1626                                                 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1627                                         }
1628
1629                                         if ($aPointPolygon['place_id'])
1630                                         {
1631                                                 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1632                                                 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1633                                                 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1634                                                 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1635
1636                                                 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1637                                                 {
1638                                                         $aResult['lat'] = $aPointPolygon['centrelat'];
1639                                                         $aResult['lon'] = $aPointPolygon['centrelon'];
1640                                                 }
1641
1642                                                 if ($this->bIncludePolygonAsPoints)
1643                                                 {
1644                                                         // Translate geometry string to point array
1645                                                         if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1646                                                         {
1647                                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1648                                                         }
1649                             /*
1650                                                         elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1651                                                         {
1652                                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1653                                                         }
1654                             */
1655                                                         elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1656                                                         {
1657                                                                 $fRadius = 0.01;
1658                                                                 $iSteps = ($fRadius * 40000)^2;
1659                                                                 $fStepSize = (2*pi())/$iSteps;
1660                                                                 $aPolyPoints = array();
1661                                                                 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1662                                                                 {
1663                                                                         $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1664                                                                 }
1665                                                                 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1666                                                                 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1667                                                                 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1668                                                                 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1669                                                         }
1670                                                 }
1671
1672                                                 // Output data suitable for display (points and a bounding box)
1673                                                 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1674                                                 {
1675                                                         $aResult['aPolyPoints'] = array();
1676                                                         foreach($aPolyPoints as $aPoint)
1677                                                         {
1678                                                                 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1679                                                         }
1680                                                 }
1681                                                 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1682                                         }
1683                                 }
1684
1685                                 if ($aResult['extra_place'] == 'city')
1686                                 {
1687                                         $aResult['class'] = 'place';
1688                                         $aResult['type'] = 'city';
1689                                         $aResult['rank_search'] = 16;
1690                                 }
1691
1692                                 if (!isset($aResult['aBoundingBox']))
1693                                 {
1694                                         // Default
1695                                         $fDiameter = 0.0001;
1696
1697                                         if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1698                                                         && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1699                                         {
1700                                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1701                                         }
1702                                         elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1703                                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1704                                         {
1705                                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1706                                         }
1707                                         $fRadius = $fDiameter / 2;
1708
1709                                         $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1710                                         $fStepSize = (2*pi())/$iSteps;
1711                                         $aPolyPoints = array();
1712                                         for($f = 0; $f < 2*pi(); $f += $fStepSize)
1713                                         {
1714                                                 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1715                                         }
1716                                         $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1717                                         $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1718                                         $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1719                                         $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1720
1721                                         // Output data suitable for display (points and a bounding box)
1722                                         if ($this->bIncludePolygonAsPoints)
1723                                         {
1724                                                 $aResult['aPolyPoints'] = array();
1725                                                 foreach($aPolyPoints as $aPoint)
1726                                                 {
1727                                                         $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1728                                                 }
1729                                         }
1730                                         $aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
1731                                 }
1732
1733                                 // Is there an icon set for this type of result?
1734                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1735                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1736                                 {
1737                                         $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1738                                 }
1739
1740                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1741                                                 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1742                                 {
1743                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1744                                 }
1745                                 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1746                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1747                                 {
1748                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1749                                 }
1750
1751                                 if ($this->bIncludeAddressDetails)
1752                                 {
1753                                         $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1754                                         if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1755                                         {
1756                                                 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1757                                         }
1758                                 }
1759
1760                                 // Adjust importance for the number of exact string matches in the result
1761                                 $aResult['importance'] = max(0.001,$aResult['importance']);
1762                                 $iCountWords = 0;
1763                                 $sAddress = $aResult['langaddress'];
1764                                 foreach($aRecheckWords as $i => $sWord)
1765                                 {
1766                                         if (stripos($sAddress, $sWord)!==false)
1767                                         {
1768                                                 $iCountWords++;
1769                                                 if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1770                                         }
1771                                 }
1772
1773                                 $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
1774
1775                                 $aResult['name'] = $aResult['langaddress'];
1776                                 // secondary ordering (for results with same importance (the smaller the better):
1777                                 //   - approximate importance of address parts
1778                                 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1779                                 //   - number of exact matches from the query
1780                                 if (isset($this->exactMatchCache[$aResult['place_id']]))
1781                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1782                                 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1783                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1784                                 //  - importance of the class/type
1785                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1786                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1787                                 {
1788                                         $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1789                                 }
1790                                 else
1791                                 {
1792                                         $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1793                                 }
1794                                 $aSearchResults[$iResNum] = $aResult;
1795                         }
1796                         uasort($aSearchResults, 'byImportance');
1797
1798                         $aOSMIDDone = array();
1799                         $aClassTypeNameDone = array();
1800                         $aToFilter = $aSearchResults;
1801                         $aSearchResults = array();
1802
1803                         $bFirst = true;
1804                         foreach($aToFilter as $iResNum => $aResult)
1805                         {
1806                                 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1807                                 if ($bFirst)
1808                                 {
1809                                         $fLat = $aResult['lat'];
1810                                         $fLon = $aResult['lon'];
1811                                         if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1812                                         $bFirst = false;
1813                                 }
1814                                 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1815                                                         && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1816                                 {
1817                                         $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1818                                         $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1819                                         $aSearchResults[] = $aResult;
1820                                 }
1821
1822                                 // Absolute limit on number of results
1823                                 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1824                         }
1825
1826                         return $aSearchResults;
1827
1828                 } // end lookup()
1829
1830
1831         } // end class
1832