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