]> git.openstreetmap.org Git - nominatim.git/blob - utils/update.php
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / utils / update.php
1 <?php
2
3 require_once(CONST_BasePath.'/lib/init-cmd.php');
4 require_once(CONST_BasePath.'/lib/setup_functions.php');
5 require_once(CONST_BasePath.'/lib/setup/SetupClass.php');
6 require_once(CONST_BasePath.'/lib/setup/AddressLevelParser.php');
7
8 ini_set('memory_limit', '800M');
9
10 use Nominatim\Setup\SetupFunctions as SetupFunctions;
11
12 // (long-opt, short-opt, min-occurs, max-occurs, num-arguments, num-arguments, type, help)
13 $aCMDOptions
14 = array(
15    'Import / update / index osm data',
16    array('help', 'h', 0, 1, 0, 0, false, 'Show Help'),
17    array('quiet', 'q', 0, 1, 0, 0, 'bool', 'Quiet output'),
18    array('verbose', 'v', 0, 1, 0, 0, 'bool', 'Verbose output'),
19
20    array('init-updates', '', 0, 1, 0, 0, 'bool', 'Set up database for updating'),
21    array('check-for-updates', '', 0, 1, 0, 0, 'bool', 'Check if new updates are available'),
22    array('no-update-functions', '', 0, 1, 0, 0, 'bool', 'Do not update trigger functions to support differential updates (assuming the diff update logic is already present)'),
23    array('import-osmosis', '', 0, 1, 0, 0, 'bool', 'Import updates once'),
24    array('import-osmosis-all', '', 0, 1, 0, 0, 'bool', 'Import updates forever'),
25    array('no-index', '', 0, 1, 0, 0, 'bool', 'Do not index the new data'),
26
27    array('calculate-postcodes', '', 0, 1, 0, 0, 'bool', 'Update postcode centroid table'),
28
29    array('import-file', '', 0, 1, 1, 1, 'realpath', 'Re-import data from an OSM file'),
30    array('import-diff', '', 0, 1, 1, 1, 'realpath', 'Import a diff (osc) file from local file system'),
31    array('osm2pgsql-cache', '', 0, 1, 1, 1, 'int', 'Cache size used by osm2pgsql'),
32
33    array('import-node', '', 0, 1, 1, 1, 'int', 'Re-import node'),
34    array('import-way', '', 0, 1, 1, 1, 'int', 'Re-import way'),
35    array('import-relation', '', 0, 1, 1, 1, 'int', 'Re-import relation'),
36    array('import-from-main-api', '', 0, 1, 0, 0, 'bool', 'Use OSM API instead of Overpass to download objects'),
37
38    array('index', '', 0, 1, 0, 0, 'bool', 'Index'),
39    array('index-rank', '', 0, 1, 1, 1, 'int', 'Rank to start indexing from'),
40    array('index-instances', '', 0, 1, 1, 1, 'int', 'Number of indexing instances (threads)'),
41
42    array('deduplicate', '', 0, 1, 0, 0, 'bool', 'Deduplicate tokens'),
43    array('recompute-word-counts', '', 0, 1, 0, 0, 'bool', 'Compute frequency of full-word search terms'),
44    array('update-address-levels', '', 0, 1, 0, 0, 'bool', 'Reimport address level configuration (EXPERT)'),
45    array('recompute-importance', '', 0, 1, 0, 0, 'bool', 'Recompute place importances'),
46    array('no-npi', '', 0, 1, 0, 0, 'bool', '(obsolete)'),
47   );
48
49 getCmdOpt($_SERVER['argv'], $aCMDOptions, $aResult, true, true);
50
51 if (!isset($aResult['index-instances'])) $aResult['index-instances'] = 1;
52
53 if (!isset($aResult['index-rank'])) $aResult['index-rank'] = 0;
54
55 date_default_timezone_set('Etc/UTC');
56
57 $oDB = new Nominatim\DB();
58 $oDB->connect();
59
60 $aDSNInfo = Nominatim\DB::parseDSN(CONST_Database_DSN);
61 if (!isset($aDSNInfo['port']) || !$aDSNInfo['port']) $aDSNInfo['port'] = 5432;
62
63 // cache memory to be used by osm2pgsql, should not be more than the available memory
64 $iCacheMemory = (isset($aResult['osm2pgsql-cache'])?$aResult['osm2pgsql-cache']:2000);
65 if ($iCacheMemory + 500 > getTotalMemoryMB()) {
66     $iCacheMemory = getCacheMemoryMB();
67     echo "WARNING: resetting cache memory to $iCacheMemory\n";
68 }
69 $sOsm2pgsqlCmd = CONST_Osm2pgsql_Binary.' -klas --number-processes 1 -C '.$iCacheMemory.' -O gazetteer -S '.CONST_Import_Style.' -d '.$aDSNInfo['database'].' -P '.$aDSNInfo['port'];
70 if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
71     $sOsm2pgsqlCmd .= ' -U ' . $aDSNInfo['username'];
72 }
73 if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
74     $sOsm2pgsqlCmd .= ' -H ' . $aDSNInfo['hostspec'];
75 }
76 $aProcEnv = null;
77 if (isset($aDSNInfo['password']) && $aDSNInfo['password']) {
78     $aProcEnv = array_merge(array('PGPASSWORD' => $aDSNInfo['password']), $_ENV);
79 }
80
81 if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
82     $sOsm2pgsqlCmd .= ' --flat-nodes '.CONST_Osm2pgsql_Flatnode_File;
83 }
84
85 $sIndexCmd = CONST_BasePath.'/nominatim/nominatim.py';
86 if (!$aResult['quiet']) {
87     $sIndexCmd .= ' -v';
88 }
89 if ($aResult['verbose']) {
90     $sIndexCmd .= ' -v';
91 }
92
93 if ($aResult['init-updates']) {
94     // sanity check that the replication URL is correct
95     $sBaseState = file_get_contents(CONST_Replication_Url.'/state.txt');
96     if ($sBaseState === false) {
97         echo "\nCannot find state.txt file at the configured replication URL.\n";
98         echo "Does the URL point to a directory containing OSM update data?\n\n";
99         fail('replication URL not reachable.');
100     }
101     // sanity check for pyosmium-get-changes
102     if (!CONST_Pyosmium_Binary) {
103         echo "\nCONST_Pyosmium_Binary not configured.\n";
104         echo "You need to install pyosmium and set up the path to pyosmium-get-changes\n";
105         echo "in your local settings file.\n\n";
106         fail('CONST_Pyosmium_Binary not configured');
107     }
108     $aOutput = 0;
109     $sCmd = CONST_Pyosmium_Binary.' --help';
110     exec($sCmd, $aOutput, $iRet);
111     if ($iRet != 0) {
112         echo "Cannot execute pyosmium-get-changes.\n";
113         echo "Make sure you have pyosmium installed correctly\n";
114         echo "and have set up CONST_Pyosmium_Binary to point to pyosmium-get-changes.\n";
115         fail('pyosmium-get-changes not found or not usable');
116     }
117
118     if (!$aResult['no-update-functions']) {
119         // instantiate setupClass to use the function therein
120         $cSetup = new SetupFunctions(array(
121                                       'enable-diff-updates' => true,
122                                       'verbose' => $aResult['verbose']
123                                      ));
124         $cSetup->connect();
125         $cSetup->createFunctions();
126     }
127
128     $sDatabaseDate = getDatabaseDate($oDB);
129     if (!$sDatabaseDate) {
130         fail('Cannot determine date of database.');
131     }
132     $sWindBack = strftime('%Y-%m-%dT%H:%M:%SZ', strtotime($sDatabaseDate) - (3*60*60));
133
134     // get the appropriate state id
135     $aOutput = 0;
136     $sCmd = CONST_Pyosmium_Binary.' -D '.$sWindBack.' --server '.CONST_Replication_Url;
137     exec($sCmd, $aOutput, $iRet);
138     if ($iRet != 0 || $aOutput[0] == 'None') {
139         fail('Error running pyosmium tools');
140     }
141
142     $oDB->exec('TRUNCATE import_status');
143     $sSQL = "INSERT INTO import_status (lastimportdate, sequence_id, indexed) VALUES('";
144     $sSQL .= $sDatabaseDate."',".$aOutput[0].', true)';
145
146     try {
147         $oDB->exec($sSQL);
148     } catch (\Nominatim\DatabaseError $e) {
149         fail('Could not enter sequence into database.');
150     }
151
152     echo "Done. Database updates will start at sequence $aOutput[0] ($sWindBack)\n";
153 }
154
155 if ($aResult['check-for-updates']) {
156     $aLastState = $oDB->getRow('SELECT sequence_id FROM import_status');
157
158     if (!$aLastState['sequence_id']) {
159         fail('Updates not set up. Please run ./utils/update.php --init-updates.');
160     }
161
162     system(CONST_BasePath.'/utils/check_server_for_updates.py '.CONST_Replication_Url.' '.$aLastState['sequence_id'], $iRet);
163     exit($iRet);
164 }
165
166 if (isset($aResult['import-diff']) || isset($aResult['import-file'])) {
167     // import diffs and files directly (e.g. from osmosis --rri)
168     $sNextFile = isset($aResult['import-diff']) ? $aResult['import-diff'] : $aResult['import-file'];
169
170     if (!file_exists($sNextFile)) {
171         fail("Cannot open $sNextFile\n");
172     }
173
174     // Import the file
175     $sCMD = $sOsm2pgsqlCmd.' '.$sNextFile;
176     echo $sCMD."\n";
177     $iErrorLevel = runWithEnv($sCMD, $aProcEnv);
178
179     if ($iErrorLevel) {
180         fail("Error from osm2pgsql, $iErrorLevel\n");
181     }
182
183     // Don't update the import status - we don't know what this file contains
184 }
185
186 if ($aResult['calculate-postcodes']) {
187     info('Update postcodes centroids');
188     $sTemplate = file_get_contents(CONST_BasePath.'/sql/update-postcodes.sql');
189     runSQLScript($sTemplate, true, true);
190 }
191
192 $sTemporaryFile = CONST_BasePath.'/data/osmosischange.osc';
193 $bHaveDiff = false;
194 $bUseOSMApi = isset($aResult['import-from-main-api']) && $aResult['import-from-main-api'];
195 $sContentURL = '';
196 if (isset($aResult['import-node']) && $aResult['import-node']) {
197     if ($bUseOSMApi) {
198         $sContentURL = 'https://www.openstreetmap.org/api/0.6/node/'.$aResult['import-node'];
199     } else {
200         $sContentURL = 'https://overpass-api.de/api/interpreter?data=node('.$aResult['import-node'].');out%20meta;';
201     }
202 }
203
204 if (isset($aResult['import-way']) && $aResult['import-way']) {
205     if ($bUseOSMApi) {
206         $sContentURL = 'https://www.openstreetmap.org/api/0.6/way/'.$aResult['import-way'].'/full';
207     } else {
208         $sContentURL = 'https://overpass-api.de/api/interpreter?data=(way('.$aResult['import-way'].');node(w););out%20meta;';
209     }
210 }
211
212 if (isset($aResult['import-relation']) && $aResult['import-relation']) {
213     if ($bUseOSMApi) {
214         $sContentURLsModifyXMLstr = 'https://www.openstreetmap.org/api/0.6/relation/'.$aResult['import-relation'].'/full';
215     } else {
216         $sContentURL = 'https://overpass-api.de/api/interpreter?data=((rel('.$aResult['import-relation'].');way(r);node(w));node(r));out%20meta;';
217     }
218 }
219
220 if ($sContentURL) {
221     file_put_contents($sTemporaryFile, file_get_contents($sContentURL));
222     $bHaveDiff = true;
223 }
224
225 if ($bHaveDiff) {
226     // import generated change file
227     $sCMD = $sOsm2pgsqlCmd.' '.$sTemporaryFile;
228     echo $sCMD."\n";
229     $iErrorLevel = runWithEnv($sCMD, $aProcEnv);
230     if ($iErrorLevel) {
231         fail("osm2pgsql exited with error level $iErrorLevel\n");
232     }
233 }
234
235 if ($aResult['deduplicate']) {
236     $oDB = new Nominatim\DB();
237     $oDB->connect();
238
239     if ($oDB->getPostgresVersion() < 9.3) {
240         fail('ERROR: deduplicate is only currently supported in postgresql 9.3');
241     }
242
243     $sSQL = 'select partition from country_name order by country_code';
244     $aPartitions = $oDB->getCol($sSQL);
245     $aPartitions[] = 0;
246
247     // we don't care about empty search_name_* partitions, they can't contain mentions of duplicates
248     foreach ($aPartitions as $i => $sPartition) {
249         $sSQL = 'select count(*) from search_name_'.$sPartition;
250         $nEntries = $oDB->getOne($sSQL);
251         if ($nEntries == 0) {
252             unset($aPartitions[$i]);
253         }
254     }
255
256     $sSQL = "select word_token,count(*) from word where substr(word_token, 1, 1) = ' '";
257     $sSQL .= ' and class is null and type is null and country_code is null';
258     $sSQL .= ' group by word_token having count(*) > 1 order by word_token';
259     $aDuplicateTokens = $oDB->getAll($sSQL);
260     foreach ($aDuplicateTokens as $aToken) {
261         if (trim($aToken['word_token']) == '' || trim($aToken['word_token']) == '-') continue;
262         echo 'Deduping '.$aToken['word_token']."\n";
263         $sSQL = 'select word_id,';
264         $sSQL .= ' (select count(*) from search_name where nameaddress_vector @> ARRAY[word_id]) as num';
265         $sSQL .= " from word where word_token = '".$aToken['word_token'];
266         $sSQL .= "' and class is null and type is null and country_code is null order by num desc";
267         $aTokenSet = $oDB->getAll($sSQL);
268
269         $aKeep = array_shift($aTokenSet);
270         $iKeepID = $aKeep['word_id'];
271
272         foreach ($aTokenSet as $aRemove) {
273             $sSQL = 'update search_name set';
274             $sSQL .= ' name_vector = array_replace(name_vector,'.$aRemove['word_id'].','.$iKeepID.'),';
275             $sSQL .= ' nameaddress_vector = array_replace(nameaddress_vector,'.$aRemove['word_id'].','.$iKeepID.')';
276             $sSQL .= ' where name_vector @> ARRAY['.$aRemove['word_id'].']';
277             $oDB->exec($sSQL);
278
279             $sSQL = 'update search_name set';
280             $sSQL .= ' nameaddress_vector = array_replace(nameaddress_vector,'.$aRemove['word_id'].','.$iKeepID.')';
281             $sSQL .= ' where nameaddress_vector @> ARRAY['.$aRemove['word_id'].']';
282             $oDB->exec($sSQL);
283
284             $sSQL = 'update location_area_country set';
285             $sSQL .= ' keywords = array_replace(keywords,'.$aRemove['word_id'].','.$iKeepID.')';
286             $sSQL .= ' where keywords @> ARRAY['.$aRemove['word_id'].']';
287             $oDB->exec($sSQL);
288
289             foreach ($aPartitions as $sPartition) {
290                 $sSQL = 'update search_name_'.$sPartition.' set';
291                 $sSQL .= ' name_vector = array_replace(name_vector,'.$aRemove['word_id'].','.$iKeepID.')';
292                 $sSQL .= ' where name_vector @> ARRAY['.$aRemove['word_id'].']';
293                 $oDB->exec($sSQL);
294
295                 $sSQL = 'update location_area_country set';
296                 $sSQL .= ' keywords = array_replace(keywords,'.$aRemove['word_id'].','.$iKeepID.')';
297                 $sSQL .= ' where keywords @> ARRAY['.$aRemove['word_id'].']';
298                 $oDB->exec($sSQL);
299             }
300
301             $sSQL = 'delete from word where word_id = '.$aRemove['word_id'];
302             $oDB->exec($sSQL);
303         }
304     }
305 }
306
307 if ($aResult['recompute-word-counts']) {
308     info('Recompute frequency of full-word search terms');
309     $sTemplate = file_get_contents(CONST_BasePath.'/sql/words_from_search_name.sql');
310     runSQLScript($sTemplate, true, true);
311 }
312
313 if ($aResult['index']) {
314     $sCmd = $sIndexCmd
315             .' -d '.$aDSNInfo['database']
316             .' -P '.$aDSNInfo['port']
317             .' -t '.$aResult['index-instances']
318             .' -r '.$aResult['index-rank'];
319     if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
320         $sCmd .= ' -H ' . $aDSNInfo['hostspec'];
321     }
322     if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
323         $sCmd .= ' -U ' . $aDSNInfo['username'];
324     }
325
326     runWithEnv($sCmd, $aProcEnv);
327
328     $oDB->exec('update import_status set indexed = true');
329 }
330
331 if ($aResult['update-address-levels']) {
332     echo 'Updating address levels from '.CONST_Address_Level_Config.".\n";
333     $oAlParser = new \Nominatim\Setup\AddressLevelParser(CONST_Address_Level_Config);
334     $oAlParser->createTable($oDB, 'address_levels');
335 }
336
337 if ($aResult['recompute-importance']) {
338     echo "Updating importance values for database.\n";
339     $oDB = new Nominatim\DB();
340     $oDB->connect();
341
342     $sSQL = 'ALTER TABLE placex DISABLE TRIGGER ALL;';
343     $sSQL .= 'UPDATE placex SET (wikipedia, importance) =';
344     $sSQL .= '   (SELECT wikipedia, importance';
345     $sSQL .= '    FROM compute_importance(extratags, country_code, osm_type, osm_id));';
346     $sSQL .= 'UPDATE placex s SET wikipedia = d.wikipedia, importance = d.importance';
347     $sSQL .= ' FROM placex d';
348     $sSQL .= ' WHERE s.place_id = d.linked_place_id and d.wikipedia is not null';
349     $sSQL .= '       and (s.wikipedia is null or s.importance < d.importance);';
350     $sSQL .= 'ALTER TABLE placex ENABLE TRIGGER ALL;';
351     $oDB->exec($sSQL);
352 }
353
354 if ($aResult['import-osmosis'] || $aResult['import-osmosis-all']) {
355     //
356     if (strpos(CONST_Replication_Url, 'download.geofabrik.de') !== false && CONST_Replication_Update_Interval < 86400) {
357         fail('Error: Update interval too low for download.geofabrik.de. ' .
358              "Please check install documentation (http://nominatim.org/release-docs/latest/Import-and-Update#setting-up-the-update-process)\n");
359     }
360
361     $sImportFile = CONST_InstallPath.'/osmosischange.osc';
362     $sCMDDownload = CONST_Pyosmium_Binary.' --server '.CONST_Replication_Url.' -o '.$sImportFile.' -s '.CONST_Replication_Max_Diff_size;
363     $sCMDImport = $sOsm2pgsqlCmd.' '.$sImportFile;
364     $sCMDIndex = $sIndexCmd
365                  .' -d '.$aDSNInfo['database']
366                  .' -P '.$aDSNInfo['port']
367                  .' -t '.$aResult['index-instances'];
368     if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
369         $sCMDIndex .= ' -H ' . $aDSNInfo['hostspec'];
370     }
371     if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
372         $sCMDIndex .= ' -U ' . $aDSNInfo['username'];
373     }
374
375     while (true) {
376         $fStartTime = time();
377         $aLastState = $oDB->getRow('SELECT *, EXTRACT (EPOCH FROM lastimportdate) as unix_ts FROM import_status');
378
379         if (!$aLastState['sequence_id']) {
380             echo "Updates not set up. Please run ./utils/update.php --init-updates.\n";
381             exit(1);
382         }
383
384         echo 'Currently at sequence '.$aLastState['sequence_id'].' ('.$aLastState['lastimportdate'].') - '.$aLastState['indexed']." indexed\n";
385
386         $sBatchEnd = $aLastState['lastimportdate'];
387         $iEndSequence = $aLastState['sequence_id'];
388
389         if ($aLastState['indexed']) {
390             // Sleep if the update interval has not yet been reached.
391             $fNextUpdate = $aLastState['unix_ts'] + CONST_Replication_Update_Interval;
392             if ($fNextUpdate > $fStartTime) {
393                 $iSleepTime = $fNextUpdate - $fStartTime;
394                 echo "Waiting for next update for $iSleepTime sec.";
395                 sleep($iSleepTime);
396             }
397
398             // Download the next batch of changes.
399             do {
400                 $fCMDStartTime = time();
401                 $iNextSeq = (int) $aLastState['sequence_id'];
402                 unset($aOutput);
403                 echo "$sCMDDownload -I $iNextSeq\n";
404                 if (file_exists($sImportFile)) {
405                     unlink($sImportFile);
406                 }
407                 exec($sCMDDownload.' -I '.$iNextSeq, $aOutput, $iResult);
408
409                 if ($iResult == 3) {
410                     echo 'No new updates. Sleeping for '.CONST_Replication_Recheck_Interval." sec.\n";
411                     sleep(CONST_Replication_Recheck_Interval);
412                 } elseif ($iResult != 0) {
413                     echo 'ERROR: updates failed.';
414                     exit($iResult);
415                 } else {
416                     $iEndSequence = (int)$aOutput[0];
417                 }
418             } while ($iResult);
419
420             // get the newest object from the diff file
421             $sBatchEnd = 0;
422             $iRet = 0;
423             exec(CONST_BasePath.'/utils/osm_file_date.py '.$sImportFile, $sBatchEnd, $iRet);
424             if ($iRet == 5) {
425                 echo "Diff file is empty. skipping import.\n";
426                 if (!$aResult['import-osmosis-all']) {
427                     exit(0);
428                 } else {
429                     continue;
430                 }
431             }
432             if ($iRet != 0) {
433                 fail('Error getting date from diff file.');
434             }
435             $sBatchEnd = $sBatchEnd[0];
436
437             // Import the file
438             $fCMDStartTime = time();
439             echo $sCMDImport."\n";
440             unset($sJunk);
441             $iErrorLevel = runWithEnv($sCMDImport, $aProcEnv);
442             if ($iErrorLevel) {
443                 echo "Error executing osm2pgsql: $iErrorLevel\n";
444                 exit($iErrorLevel);
445             }
446
447             // write the update logs
448             $iFileSize = filesize($sImportFile);
449             $sSQL = 'INSERT INTO import_osmosis_log';
450             $sSQL .= '(batchend, batchseq, batchsize, starttime, endtime, event)';
451             $sSQL .= " values ('$sBatchEnd',$iEndSequence,$iFileSize,'";
452             $sSQL .= date('Y-m-d H:i:s', $fCMDStartTime)."','";
453             $sSQL .= date('Y-m-d H:i:s')."','import')";
454             var_Dump($sSQL);
455             $oDB->exec($sSQL);
456
457             // update the status
458             $sSQL = "UPDATE import_status SET lastimportdate = '$sBatchEnd', indexed=false, sequence_id = $iEndSequence";
459             var_Dump($sSQL);
460             $oDB->exec($sSQL);
461             echo date('Y-m-d H:i:s')." Completed download step for $sBatchEnd in ".round((time()-$fCMDStartTime)/60, 2)." minutes\n";
462         }
463
464         // Index file
465         if (!$aResult['no-index']) {
466             $sThisIndexCmd = $sCMDIndex;
467             $fCMDStartTime = time();
468
469             echo "$sThisIndexCmd\n";
470             $iErrorLevel = runWithEnv($sThisIndexCmd, $aProcEnv);
471             if ($iErrorLevel) {
472                 echo "Error: $iErrorLevel\n";
473                 exit($iErrorLevel);
474             }
475
476             $sSQL = 'INSERT INTO import_osmosis_log';
477             $sSQL .= '(batchend, batchseq, batchsize, starttime, endtime, event)';
478             $sSQL .= " values ('$sBatchEnd',$iEndSequence,NULL,'";
479             $sSQL .= date('Y-m-d H:i:s', $fCMDStartTime)."','";
480             $sSQL .= date('Y-m-d H:i:s')."','index')";
481             var_Dump($sSQL);
482             $oDB->exec($sSQL);
483             echo date('Y-m-d H:i:s')." Completed index step for $sBatchEnd in ".round((time()-$fCMDStartTime)/60, 2)." minutes\n";
484
485             $sSQL = 'update import_status set indexed = true';
486             $oDB->exec($sSQL);
487         } else {
488             if ($aResult['import-osmosis-all']) {
489                 echo "Error: --no-index cannot be used with continuous imports (--import-osmosis-all).\n";
490                 exit(1);
491             }
492         }
493
494         $fDuration = time() - $fStartTime;
495         echo date('Y-m-d H:i:s')." Completed all for $sBatchEnd in ".round($fDuration/60, 2)." minutes\n";
496         if (!$aResult['import-osmosis-all']) exit(0);
497     }
498 }