diff --git a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf.java b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf.java
deleted file mode 100644
index 048b8bcfd52e099363a5962f43a45bc6e3f31ab4..0000000000000000000000000000000000000000
--- a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package gov.usgs.earthquake.nshmp.netcdf;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Map;
-
-import gov.usgs.earthquake.nshmp.data.XySequence;
-import gov.usgs.earthquake.nshmp.geo.Location;
-import gov.usgs.earthquake.nshmp.gmm.Imt;
-
-public interface Netcdf {
-
-  /**
-   * Returns a {@code NetCdf} reader for 2018 hazard curves.
-   * 
-   * @param path The path to the 2018 Netcdf hazard file
-   * @throws IOException
-   */
-  public static Netcdf create2018Reader(Path path) {
-    return new Netcdf2018Reader(path);
-  }
-
-  /**
-   * Returns the Netcdf dimensions and coordinate variables.
-   */
-  NetcdfCoordinates coordinates();
-
-  /**
-   * Returns the path to the Netcdf file.
-   */
-  Path path();
-
-  /**
-   * Return the full set of hazard curves at the bounding grid points and the
-   * target Location. Intended for validation uses.
-   * 
-   * @param site The location to get bouning hazards
-   */
-  List<Map<SiteClass, Map<Imt, XySequence>>> boundingHazards(Location site);
-
-  /**
-   * Return a Map<SiteClass, Map<Imt, XySequence>> with hazard curves at the
-   * specified Location.
-   * 
-   * @param site The site to get the hazard curves
-   */
-  Map<SiteClass, Map<Imt, XySequence>> hazard(Location site);
-
-  /**
-   * Return a single hazard curve for the specified Location, SiteClass, and
-   * Imt.
-   * 
-   * @param site The site to get the hazard curves
-   * @param siteClass The site class
-   * @param imt The IMT
-   */
-  XySequence hazard(Location site, SiteClass siteClass, Imt imt);
-
-}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NshmGroup.java b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NshmGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..b6470018837709aa169efb38df4104774e019b2b
--- /dev/null
+++ b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NshmGroup.java
@@ -0,0 +1,21 @@
+package gov.usgs.earthquake.nshmp.netcdf;
+
+public enum NshmGroup {
+
+  NSHM_CONUS_2018("/CONUS/2018/v1.1");
+
+  private final String baseGroup;
+
+  private NshmGroup(String baseGroup) {
+    this.baseGroup = baseGroup;
+  }
+
+  /**
+   * Returns the Netcdf target base group to read.
+   */
+  @Override
+  public String toString() {
+    return baseGroup;
+  }
+
+}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf2018Reader.java b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NshmNetcdfReader.java
similarity index 57%
rename from src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf2018Reader.java
rename to src/main/java/gov/usgs/earthquake/nshmp/netcdf/NshmNetcdfReader.java
index 0e482fdb1bc193223d6c638896110e85b3fae7e2..6cbdd8d62f23031955771b7a00367ad953b62bc9 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf2018Reader.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NshmNetcdfReader.java
@@ -5,10 +5,14 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.netcdf.reader.BoundingHazards;
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfCoordinates;
 
 import ucar.nc2.dataset.NetcdfDataset;
 
@@ -33,14 +37,21 @@ import ucar.nc2.dataset.NetcdfDataset;
  * 
  * @author U.S. Geological Survey
  */
-class Netcdf2018Reader implements Netcdf {
+public class NshmNetcdfReader {
 
-  private static final String BASE_GROUP = "/CONUS/2018/v1.1";
+  private static final Logger LOGGER = Logger.getLogger("ucar");
 
   private final Path path;
+  private final NshmGroup nshmGroup;
   private final NetcdfCoordinates coords;
 
-  Netcdf2018Reader(Path path) {
+  static {
+    /* Update ucar logger */
+    LOGGER.setLevel(Level.SEVERE);
+  }
+
+  public NshmNetcdfReader(NshmGroup nshmGroup, Path path) {
+    this.nshmGroup = nshmGroup;
     this.path = path;
 
     if (Files.notExists(path)) {
@@ -52,34 +63,62 @@ class Netcdf2018Reader implements Netcdf {
       // - Handle metadata from netCDF file get root group and
       // attributes, and attributes of subgroups
       // - Get netCDF dimensions
-      var targetGroup = ncd.findGroup(BASE_GROUP);
+      var targetGroup = ncd.findGroup(nshmGroup.toString());
       coords = new NetcdfCoordinates(targetGroup);
     } catch (IOException e) {
       throw new RuntimeException("Could not read Netcdf file [" + path + " ]");
     }
   }
 
-  @Override
+  /**
+   * Returns the NSHM NetCDF group
+   */
+  public NshmGroup nshmGroup() {
+    return nshmGroup;
+  }
+
+  /**
+   * Returns the NetCDF dimensions and coordinate variables.
+   */
   public NetcdfCoordinates coordinates() {
     return coords;
   }
 
-  @Override
+  /**
+   * Returns the path to the NetCDF file.
+   */
   public Path path() {
     return path;
   }
 
-  @Override
+  /**
+   * Return the full set of hazard curves at the bounding grid points and the
+   * target Location. Intended for validation uses.
+   * 
+   * @param site The location to get bounding hazards
+   */
   public List<Map<SiteClass, Map<Imt, XySequence>>> boundingHazards(Location site) {
-    return BoundingHazards.boundingHazards(this, site, BASE_GROUP);
+    return BoundingHazards.boundingHazards(this, site);
   }
 
-  @Override
+  /**
+   * Return a Map<SiteClass, Map<Imt, XySequence>> with hazard curves at the
+   * specified Location.
+   * 
+   * @param site The site to get the hazard curves
+   */
   public Map<SiteClass, Map<Imt, XySequence>> hazard(Location site) {
     return boundingHazards(site).get(4);
   }
 
-  @Override
+  /**
+   * Return a single hazard curve for the specified Location, SiteClass, and
+   * Imt.
+   * 
+   * @param site The site to get the hazard curves
+   * @param siteClass The site class
+   * @param imt The IMT
+   */
   public XySequence hazard(Location site, SiteClass siteClass, Imt imt) {
     return hazard(site).get(siteClass).get(imt);
   }
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/BoundingHazards.java b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/BoundingHazards.java
similarity index 87%
rename from src/main/java/gov/usgs/earthquake/nshmp/netcdf/BoundingHazards.java
rename to src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/BoundingHazards.java
index f89e2cf7898fb92411ac7209c331636b7d78ff50..769db9dcd5c805f1e409c33ad65c7bba58025d6b 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/BoundingHazards.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/BoundingHazards.java
@@ -1,4 +1,4 @@
-package gov.usgs.earthquake.nshmp.netcdf;
+package gov.usgs.earthquake.nshmp.netcdf.reader;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -11,7 +11,9 @@ import com.google.common.collect.Maps;
 import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
-import gov.usgs.earthquake.nshmp.netcdf.NetcdfUtils.Key;
+import gov.usgs.earthquake.nshmp.netcdf.NshmNetcdfReader;
+import gov.usgs.earthquake.nshmp.netcdf.SiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils.Key;
 
 import ucar.ma2.Array;
 import ucar.ma2.DataType;
@@ -23,29 +25,38 @@ import ucar.nc2.dataset.NetcdfDataset;
 /*
  * Container for gridded hazard curves at four closest grid points to target
  */
-class BoundingHazards {
+public class BoundingHazards {
 
-  final Netcdf netcdf;
-  final String baseGroup;
-  final NetcdfCoordinates coords;
+  private final NshmNetcdfReader netcdf;
+  private final NetcdfCoordinates coords;
+  private List<Map<SiteClass, Map<Imt, XySequence>>> boundingHazards;
 
-  private int idxLonLL;
-  private int idxLatLL;
-
-  final List<Map<SiteClass, Map<Imt, XySequence>>> boundingHazards;
-
-  private BoundingHazards(Netcdf netcdf, Location site, String baseGroup) {
+  private BoundingHazards(NshmNetcdfReader netcdf, Location site) {
     this.netcdf = netcdf;
-    this.baseGroup = baseGroup;
     this.coords = netcdf.coordinates();
+    coords.contains(site);
+    setBoundingHazards(site);
+  }
 
-    coords.checkCoords(site);
+  /**
+   * Returns the bounding hazards at four closet grid points to target.
+   * 
+   * @param netcdf The {@code Netcdf}
+   * @param site The site to get bounding hazards
+   * @param baseGroup The Netcdf base group
+   */
+  public static List<Map<SiteClass, Map<Imt, XySequence>>> boundingHazards(
+      NshmNetcdfReader netcdf,
+      Location site) {
+    return new BoundingHazards(netcdf, site).boundingHazards;
+  }
 
+  private void setBoundingHazards(Location site) {
     var longitudes = coords.locations().stream().mapToDouble(loc -> loc.longitude).toArray();
     var latitudes = coords.locations().stream().mapToDouble(loc -> loc.latitude).toArray();
 
-    idxLonLL = NetcdfUtils.getIdxLTEQ(longitudes, site.longitude);
-    idxLatLL = NetcdfUtils.getIdxLTEQ(latitudes, site.latitude);
+    var idxLonLL = NetcdfUtils.getIdxLTEQ(longitudes, site.longitude);
+    var idxLatLL = NetcdfUtils.getIdxLTEQ(latitudes, site.latitude);
 
     boundingHazards = extractHazardsAt(idxLonLL, idxLatLL);
 
@@ -54,20 +65,6 @@ class BoundingHazards {
     boundingHazards.add(calcTargetHazards(fracLon, fracLat));
   }
 
-  /**
-   * Returns the bounding hazards at four closet grid points to target.
-   * 
-   * @param netcdf The {@code Netcdf}
-   * @param site The site to get bounding hazards
-   * @param baseGroup The Netcdf base group
-   */
-  static List<Map<SiteClass, Map<Imt, XySequence>>> boundingHazards(
-      Netcdf netcdf,
-      Location site,
-      String baseGroup) {
-    return new BoundingHazards(netcdf, site, baseGroup).boundingHazards;
-  }
-
   private Map<SiteClass, Map<Imt, XySequence>> calcTargetHazards(double fracLon, double fracLat) {
     var westTarget = getTargetData(boundingHazards.get(0), boundingHazards.get(1), fracLat);
     var eastTarget = getTargetData(boundingHazards.get(3), boundingHazards.get(2), fracLat);
@@ -82,11 +79,10 @@ class BoundingHazards {
    * empty slot for the interpolated target hazards
    */
   private List<Map<SiteClass, Map<Imt, XySequence>>> extractHazardsAt(int idxLonLL, int idxLatLL) {
-
     var boundingHazardMaps = new ArrayList<Map<SiteClass, Map<Imt, XySequence>>>(5);
 
     try (NetcdfDataset ncd = NetcdfDataset.openDataset(netcdf.path().toString())) {
-      Group targetGroup = ncd.findGroup(baseGroup);
+      Group targetGroup = ncd.findGroup(netcdf.nshmGroup().toString());
 
       // TODO: rename variable in netCDF
       Variable vHazards = targetGroup.findVariable(Key.AEPS);
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfCoordinates.java b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfCoordinates.java
similarity index 67%
rename from src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfCoordinates.java
rename to src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfCoordinates.java
index 43c6d3910479b17840b13dd4fe7ba4eee2fba764..9627d73d4229c5574605704d764c89cd2a32a3ad 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfCoordinates.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfCoordinates.java
@@ -1,4 +1,4 @@
-package gov.usgs.earthquake.nshmp.netcdf;
+package gov.usgs.earthquake.nshmp.netcdf.reader;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
@@ -13,8 +13,11 @@ import com.google.common.collect.Maps;
 
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.geo.LocationList;
+import gov.usgs.earthquake.nshmp.geo.Region;
+import gov.usgs.earthquake.nshmp.geo.Regions;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
-import gov.usgs.earthquake.nshmp.netcdf.NetcdfUtils.Key;
+import gov.usgs.earthquake.nshmp.netcdf.SiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils.Key;
 
 import ucar.ma2.DataType;
 import ucar.ma2.InvalidRangeException;
@@ -22,7 +25,7 @@ import ucar.nc2.Group;
 import ucar.nc2.Variable;
 
 /*
- * Container for dimensions and coordinate variables of nshmp netCDF file to
+ * Container for dimensions and coordinate variables of NSHMP NetCDF file to
  * facilitate indexing operations prior to data retrieval.
  */
 public class NetcdfCoordinates {
@@ -32,23 +35,17 @@ public class NetcdfCoordinates {
   private final LocationList locations;
   private final Map<SiteClass, Map<Imt, double[]>> imls;
   private final int nIml;
+  private final Region region;
 
-  NetcdfCoordinates(Group targetGroup) throws IOException {
+  public NetcdfCoordinates(Group targetGroup) throws IOException {
     // This bypasses the netCDF dimensions, but since we know what the
     // variables and their dimensions should be, this is OK(???)
 
-    // get coordinate variables
-    var vVs30s = targetGroup.findVariable(Key.VS30);
-    var vImts = targetGroup.findVariable(Key.IMT);
-    var vlats = targetGroup.findVariable(Key.LAT);
-    var vlons = targetGroup.findVariable(Key.LON);
     var vImls = targetGroup.findVariable(Key.IMLS);
-
-    // read coordinate variables into java arrays
-    var vs30s = (int[]) vVs30s.read().get1DJavaArray(DataType.INT);
-    var imtVals = (double[]) vImts.read().get1DJavaArray(DataType.DOUBLE);
-    var lats = (double[]) vlats.read().get1DJavaArray(DataType.DOUBLE);
-    var lons = (double[]) vlons.read().get1DJavaArray(DataType.DOUBLE);
+    var vs30s = NetcdfUtils.getIntArray(targetGroup, Key.VS30);
+    var imtVals = NetcdfUtils.getDoubleArray(targetGroup, Key.IMT);
+    var lats = NetcdfUtils.getDoubleArray(targetGroup, Key.LAT);
+    var lons = NetcdfUtils.getDoubleArray(targetGroup, Key.LON);
 
     var builder = LocationList.builder();
     for (int i = 0; i < lons.length; i++) {
@@ -56,6 +53,7 @@ public class NetcdfCoordinates {
     }
 
     locations = builder.build();
+    region = Regions.createRectangular("Bounds", locations.bounds().min, locations.bounds().max);
 
     // vImls has dimensions (vs30, Imt, Iml)
     // alternatively get nIml from Dimension Iml
@@ -76,42 +74,51 @@ public class NetcdfCoordinates {
     imls = mapImls(vImls);
   }
 
-  LocationList locations() {
+  /**
+   * Returns the locations associated with a {@code NshmGroup}
+   */
+  public LocationList locations() {
     return LocationList.copyOf(locations);
   }
 
-  int nIml() {
+  /**
+   * Returns the number of Imls associated with a {@code NshmGroup}.
+   */
+  public int nIml() {
     return nIml;
   }
 
-  List<SiteClass> siteClasses() {
+  /**
+   * Return the site classes associated with a {@code NshmGroup}.
+   */
+  public List<SiteClass> siteClasses() {
     return List.copyOf(siteClasses);
   }
 
-  List<Imt> imt() {
+  /**
+   * Return the Imts associated with a {@code NshmGroup}.
+   */
+  public List<Imt> imt() {
     return List.copyOf(imts);
   }
 
-  Map<SiteClass, Map<Imt, double[]>> iml() {
+  /**
+   * Returns the Imls associated with a {@code NshmGroup}.
+   */
+  public Map<SiteClass, Map<Imt, double[]>> iml() {
     return imls;
   }
 
-  /*
-   * validate target coordinates
+  /**
+   * Validate a target site is contained with in the bounds.
+   * 
+   * @param site The site to test
    */
-  void checkCoords(Location site) {
+  public void contains(Location site) {
     var bounds = locations.bounds();
-    var minLon = bounds.min.longitude;
-    var maxLon = bounds.max.longitude;
-    var minLat = bounds.min.latitude;
-    var maxLat = bounds.max.latitude;
-    var lon = site.longitude;
-    var lat = site.latitude;
-
     checkArgument(
-        (lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat),
-        String.format("Target coordinates out of range, %.3f <= lon < %.3f, %.3f <= lat <= %.3f",
-            minLon, maxLat, minLat, maxLat));
+        region.contains(site),
+        String.format("Target site [%s] out of range %s", site.toString(), bounds.toString()));
   }
 
   /*
@@ -123,7 +130,6 @@ public class NetcdfCoordinates {
    * hazard curves...
    */
   private Map<SiteClass, Map<Imt, double[]>> mapImls(Variable vImls) {
-
     EnumMap<SiteClass, Map<Imt, double[]>> vsImtImlMap = Maps.newEnumMap(SiteClass.class);
 
     for (int i = 0; i < siteClasses.size(); i++) {
@@ -138,7 +144,8 @@ public class NetcdfCoordinates {
         var shape = new int[] { 1, 1, nIml };
 
         try {
-          imtImlMap.put(imt,
+          imtImlMap.put(
+              imt,
               (double[]) vImls.read(origin, shape).reduce().get1DJavaArray(DataType.DOUBLE));
         } catch (IOException | InvalidRangeException e) {
           var msg = "Failed read attempt for vImls with origin: " +
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfUtils.java b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfUtils.java
similarity index 72%
rename from src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfUtils.java
rename to src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfUtils.java
index 28a712c6a78df6240272657a9b8687661eb6fcd6..3e7d1b15ee044c75029db56bd2748de7b7f47004 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfUtils.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfUtils.java
@@ -1,5 +1,8 @@
-package gov.usgs.earthquake.nshmp.netcdf;
+package gov.usgs.earthquake.nshmp.netcdf.reader;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.Map;
 
@@ -8,9 +11,52 @@ import com.google.common.math.DoubleMath;
 
 import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.netcdf.SiteClass;
+
+import ucar.ma2.DataType;
+import ucar.nc2.Group;
 
 class NetcdfUtils {
 
+  /**
+   * Returns a {@code double[]} from a NetCDF group
+   * 
+   * @param group The NetCDF group
+   * @param key The key to read from the group
+   * @throws IOException
+   */
+  static double[] getDoubleArray(Group group, String key) throws IOException {
+    return (double[]) get1DArray(group, key, DataType.DOUBLE);
+  }
+
+  /**
+   * Returns a {@code int[]} from a NetCDF group
+   * 
+   * @param group The NetCDF group
+   * @param key The key to read from the group
+   * @throws IOException
+   */
+  static int[] getIntArray(Group group, String key) throws IOException {
+    return (int[]) get1DArray(group, key, DataType.INT);
+  }
+
+  /**
+   * Get a 1D array from a NetCDF group.
+   * 
+   * @param group The NetCDF group
+   * @param key The key to read from the group
+   * @param dataType The data type to read
+   * @throws IOException
+   */
+  static Object get1DArray(Group group, String key, DataType dataType) throws IOException {
+    var var = group.findVariable(key);
+    checkNotNull(
+        var,
+        String.format("Could not find variable [%s] in group [%s]", key, group.getFullName()));
+
+    return var.read().get1DJavaArray(dataType);
+  }
+
   /*
    * find index of first element in a (sorted ascending) that is less than or
    * equal to target value t. If target value is equal to the maximum value in a