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