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