docs: public void loadFromFile(ZipFile zip, String fid) throws IOException {
          if (this.loaded) throw new UnsupportedOperationException("Attempt to load GTFS into existing database");
  
          // NB we don't have a single CRC for the file, so we combine all the CRCs of the component files. NB we are not
          // simply summing the CRCs because CRCs are (I assume) uniformly randomly distributed throughout the width of a
          // long, so summing them is a convolution which moves towards a Gaussian with mean 0 (i.e. more concentrated
          // probability in the center), degrading the quality of the hash. Instead we XOR. Assuming each bit is independent,
          // this will yield a nice uniformly distributed result, because when combining two bits there is an equal
          // probability of any input, which means an equal probability of any output. At least I think that's all correct.
          // Repeated XOR is not commutative but zip.stream returns files in the order they are in the central directory
          // of the zip file, so that's not a problem.
          checksum = zip.stream().mapToLong(ZipEntry::getCrc).reduce((l1, l2) -> l1 ^ l2).getAsLong();
  
          db.getAtomicLong("checksum").set(checksum);
  
          new FeedInfo.Loader(this).loadTable(zip);
          // maybe we should just point to the feed object itself instead of its ID, and null out its stoptimes map after loading
          if (fid != null) {
              feedId = fid;
              LOG.info("Feed ID is undefined, pester maintainers to include a feed ID. Using file name {}.", feedId); // TODO log an error, ideally feeds should include a feedID
          }
          else if (feedId == null || feedId.isEmpty()) {
              feedId = new File(zip.getName()).getName().replaceAll("\\.zip$", "");
              LOG.info("Feed ID is undefined, pester maintainers to include a feed ID. Using file name {}.", feedId); // TODO log an error, ideally feeds should include a feedID
          }
          else {
              LOG.info("Feed ID is '{}'.", feedId);
          }
  
          db.getAtomicString("feed_id").set(feedId);
  
          new Agency.Loader(this).loadTable(zip);
          if (agency.isEmpty()) {
              errors.add(new GeneralError("agency", 0, "agency_id", "Need at least one agency."));
          }
  
          // calendars and calendar dates are joined into services. This means a lot of manipulating service objects as
          // they are loaded; since mapdb keys/values are immutable, load them in memory then copy them to MapDB once
          // we're done loading them
          Map<String, Service> serviceTable = new HashMap<>();
          new Calendar.Loader(this, serviceTable).loadTable(zip);
          new CalendarDate.Loader(this, serviceTable).loadTable(zip);
          this.services.putAll(serviceTable);
          serviceTable = null; // free memory
  
          // Same deal
          Map<String, Fare> fares = new HashMap<>();
          new FareAttribute.Loader(this, fares).loadTable(zip);
          new FareRule.Loader(this, fares).loadTable(zip);
          this.fares.putAll(fares);
          fares = null; // free memory
  
          new Route.Loader(this).loadTable(zip);
          new ShapePoint.Loader(this).loadTable(zip);
          new Stop.Loader(this).loadTable(zip);
          new Transfer.Loader(this).loadTable(zip);
          new Trip.Loader(this).loadTable(zip);
          new Frequency.Loader(this).loadTable(zip);
          new StopTime.Loader(this).loadTable(zip); // comment out this line for quick testing using NL feed
          loaded = true;
      }