diff --git a/bindings/java/src/org/sleuthkit/datamodel/Account.java b/bindings/java/src/org/sleuthkit/datamodel/Account.java
index bec66e453181c5f31af7afebf726adfb0ddb81c9..8db44d7d02c22993cea56f77e564d3b7eedbf7f5 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Account.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Account.java
@@ -99,7 +99,7 @@ public static final class Type {
 		 * @param typeName    The type name.
 		 * @param displayName The display name for the type.
 		 */
-		Type(String typeName, String displayName) {
+		public Type(String typeName, String displayName) {
 			this.typeName = typeName;
 			this.displayName = displayName;
 		}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
index f448236346dcd6466fdadf4b59d6c98b73687473..58c85b7ec844400ec87cac021a2fc4a3c71b11dd 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
@@ -1373,17 +1373,13 @@ public enum ATTRIBUTE_TYPE {
 				bundle.getString("BlackboardAttribute.tskattachments.text"),
 				TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON),
 		
-		TSK_GEO_DISTANCE_FROM_HOME_POINT(142, "TSK_GEO_DISTANCE_FROM_HOME_POINT", //NON-NLS
-				bundle.getString("BlackboardAttribute.tskdronehpdistance.text"),
-				TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE),
-	
-		TSK_GEO_DISTANCE_TRAVELED(143, "TSK_GEO_DISTANCE_TRAVELED", //NON-NLS
-				bundle.getString("BlackboardAttribute.tskdronedistancetraveled.text"),
-				TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE),
-		
-		TSK_GEO_TRACKPOINTS(144, "TSK_GEO_TRACKPOINTS",
+		TSK_GEO_TRACKPOINTS(142, "TSK_GEO_TRACKPOINTS",
 			bundle.getString("BlackboardAttribute.tskgeopath.text"),
 			TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON),
+		
+		TSK_GEO_WAYPOINTS(143, "TSK_GEO_WAYPOINTS",
+			bundle.getString("BlackboardAttribute.tskgeowaypoints.text"),
+			TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON),
 
 		;
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
index 3908626177bb626ce7028f5880bd815a01897b84..96584aa91e722ee42eeaae568c339dc7c13f736c 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
+++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
@@ -191,9 +191,8 @@ BlackboardAttribute.tskaccountsettings.text=Account Settings
 BlackboardAttribute.tskpasswordhint.text=Password Hint
 BlackboardAttribute.tskgroups.text=Groups
 BlackboardAttribute.tskattachments.text=Message Attachments
-BlackboardAttribute.tskdronehpdistance.text=Distance From Home Point
-BlackboardAttribute.tskdronedistancetraveled.text=Total Distance Traveled
-BlackboardAttribute.tskgeopath.text=Waypoint List
+BlackboardAttribute.tskgeopath.text=List of Track Points
+BlackboardAttribute.tskgeowaypoints.text=List of Waypoints
 AbstractFile.readLocal.exception.msg4.text=Error reading local file\: {0}
 AbstractFile.readLocal.exception.msg1.text=Error reading local file, local path is not set
 AbstractFile.readLocal.exception.msg2.text=Error reading local file, it does not exist at local path\: {0}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
index f468d6a5f63239c9195fdd6e51362bc2aec87833..e4bdea274df1d5b1d1ce04ec707a93c08d278523 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
@@ -1,7 +1,7 @@
 /*
  * Sleuth Kit Data Model
  *
- * Copyright 2017-18 Basis Technology Corp.
+ * Copyright 2017-2020 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,6 +33,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.SleuthkitCase.CaseDbConnection;
 import static org.sleuthkit.datamodel.SleuthkitCase.closeResultSet;
 import static org.sleuthkit.datamodel.SleuthkitCase.closeStatement;
@@ -48,20 +49,18 @@ public final class CommunicationsManager {
 	private final SleuthkitCase db;
 
 	private final Map<Account.Type, Integer> accountTypeToTypeIdMap
-			= new ConcurrentHashMap<Account.Type, Integer>();
+			= new ConcurrentHashMap<>();
 	private final Map<String, Account.Type> typeNameToAccountTypeMap
-			= new ConcurrentHashMap<String, Account.Type>();
-
-	// Artifact types that represent a relationship between accounts 
-	private final static Set<Integer> RELATIONSHIP_ARTIFACT_TYPE_IDS
-			= new HashSet<Integer>(Arrays.asList(
-					BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(),
-					BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(),
-					BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID(),
-					BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG.getTypeID()
-			));
-	private static final String RELATIONSHIP_ARTIFACT_TYPE_IDS_CSV_STR
-			= StringUtils.buildCSVString(RELATIONSHIP_ARTIFACT_TYPE_IDS);
+			= new ConcurrentHashMap<>();
+
+	// Artifact types that can represent a relationship between accounts. 
+	private static final Set<Integer> RELATIONSHIP_ARTIFACT_TYPE_IDS = new HashSet<Integer>(Arrays.asList(
+			BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(),
+			BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(),
+			BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID(),
+			BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG.getTypeID()
+	));
+	private static final String RELATIONSHIP_ARTIFACT_TYPE_IDS_CSV_STR = StringUtils.buildCSVString(RELATIONSHIP_ARTIFACT_TYPE_IDS);
 
 	/**
 	 * Construct a CommunicationsManager for the given SleuthkitCase.
@@ -328,7 +327,7 @@ public Account getAccount(org.sleuthkit.datamodel.Account.Type accountType, Stri
 
 	/**
 	 * Adds relationships between the sender and each of the recipient account
-	 * instances and between all recipient account instances. All account 
+	 * instances and between all recipient account instances. All account
 	 * instances must be from the same data source.
 	 *
 	 * @param sender           sender account
@@ -450,36 +449,36 @@ private Account getOrCreateAccount(Account.Type accountType, String accountUniqu
 	}
 
 	/**
-	 * Get the blackboard artifact for the given account type, account ID, and
-	 * source file. Create an artifact if it doesn't already exist.
+	 * Gets or creates an account artifact for an instance of an account found
+	 * in a file.
 	 *
-	 * @param accountType     account type
-	 * @param accountUniqueID Unique account ID (such as email address)
-	 * @param moduleName      module name that found this instance (for the
-	 *                        artifact)
-	 * @param sourceFile		    Source file (for the artifact)
+	 * @param accountType     The account type of the account instance.
+	 * @param accountUniqueID The account ID of the account instance, should be
+	 *                        unique for the account type (e.g., an email
+	 *                        address for an email account).
+	 * @param moduleName      The name of the module that found the account
+	 *                        instance.
+	 * @param sourceFile      The file in which the account instance was found.
 	 *
-	 * @return blackboard artifact for the account file instance
+	 * @return The account artifact.
 	 *
-	 * @throws TskCoreException exception thrown if a critical error occurs
-	 *                          within TSK core
+	 * @throws TskCoreException If there is an error querying or updating the
+	 *                          case database.
 	 */
-	BlackboardArtifact getOrCreateAccountFileInstanceArtifact(Account.Type accountType, String accountUniqueID, String moduleName, Content sourceFile) throws TskCoreException {
-
-		// see if it already exists
+	private BlackboardArtifact getOrCreateAccountFileInstanceArtifact(Account.Type accountType, String accountUniqueID, String moduleName, Content sourceFile) throws TskCoreException {
 		BlackboardArtifact accountArtifact = getAccountFileInstanceArtifact(accountType, accountUniqueID, sourceFile);
-		if (null != accountArtifact) {
-			return accountArtifact;
+		if (accountArtifact == null) {
+			accountArtifact = db.newBlackboardArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT, sourceFile.getId());
+			Collection<BlackboardAttribute> attributes = new ArrayList<>();
+			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE, moduleName, accountType.getTypeName()));
+			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID, moduleName, accountUniqueID));
+			accountArtifact.addAttributes(attributes);
+			try {
+				db.getBlackboard().postArtifact(accountArtifact, moduleName);
+			} catch (BlackboardException ex) {
+				LOGGER.log(Level.SEVERE, String.format("Error posting new account artifact to the blackboard (object ID = %d)", accountArtifact.getId()), ex);
+			}
 		}
-
-		// Create a new artifact.
-		accountArtifact = db.newBlackboardArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT, sourceFile.getId());
-
-		Collection<BlackboardAttribute> attributes = new ArrayList<BlackboardAttribute>();
-		attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE, moduleName, accountType.getTypeName()));
-		attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID, moduleName, accountUniqueID));
-		accountArtifact.addAttributes(attributes);
-
 		return accountArtifact;
 	}
 
@@ -662,11 +661,11 @@ public List<AccountDeviceInstance> getAccountDeviceInstancesWithRelationships(Co
 					CommunicationsFilter.RelationshipTypeFilter.class.getName()
 			));
 			String relationshipFilterSQL = getCommunicationsFilterSQL(filter, applicableInnerQueryFilters);
-			
+
 			String relationshipLimitSQL = getMostRecentFilterLimitSQL(filter);
-			
-			String relTblfilterQuery = 
-					"SELECT * "
+
+			String relTblfilterQuery
+					= "SELECT * "
 					+ "FROM account_relationships as relationships"
 					+ (relationshipFilterSQL.isEmpty() ? "" : " WHERE " + relationshipFilterSQL)
 					+ (relationshipLimitSQL.isEmpty() ? "" : relationshipLimitSQL);
@@ -901,17 +900,17 @@ public long getRelationshipSourcesCount(AccountDeviceInstance accountDeviceInsta
 
 		try {
 			s = connection.createStatement();
-			
+
 			String innerQuery = " account_relationships AS relationships";
 			String limitStr = getMostRecentFilterLimitSQL(filter);
-			
-			if(!limitStr.isEmpty()) {
+
+			if (!limitStr.isEmpty()) {
 				innerQuery = "(SELECT * FROM account_relationships as relationships " + limitStr + ") as relationships";
 			}
 
 			String queryStr
 					= "SELECT count(DISTINCT relationships.relationship_source_obj_id) as count "
-					+ "	FROM" + innerQuery 
+					+ "	FROM" + innerQuery
 					+ " WHERE relationships.data_source_obj_id IN ( " + datasourceObjIdsCSV + " )"
 					+ " AND ( relationships.account1_id = " + account_id
 					+ "      OR  relationships.account2_id = " + account_id + " )"
@@ -935,7 +934,8 @@ public long getRelationshipSourcesCount(AccountDeviceInstance accountDeviceInsta
 	 * with accounts on specific devices (AccountDeviceInstance) that meet the
 	 * filter criteria.
 	 *
-	 * Applicable filters: RelationshipTypeFilter, DateRangeFilter, MostRecentFilter
+	 * Applicable filters: RelationshipTypeFilter, DateRangeFilter,
+	 * MostRecentFilter
 	 *
 	 * @param accountDeviceInstanceList set of account device instances for
 	 *                                  which to get the relationship sources.
@@ -985,10 +985,10 @@ public Set<Content> getRelationshipSources(Set<AccountDeviceInstance> accountDev
 						.getName()
 		));
 		String filterSQL = getCommunicationsFilterSQL(filter, applicableFilters);
-		
+
 		String limitQuery = " account_relationships AS relationships";
 		String limitStr = getMostRecentFilterLimitSQL(filter);
-		if(!limitStr.isEmpty()) {
+		if (!limitStr.isEmpty()) {
 			limitQuery = "(SELECT * FROM account_relationships as relationships " + limitStr + ") as relationships";
 		}
 
@@ -1152,8 +1152,8 @@ public List<AccountDeviceInstance> getRelatedAccountDeviceInstances(AccountDevic
 	 * Get the sources (artifacts, content) of relationships between the given
 	 * account device instances.
 	 *
-	 * Applicable filters: DeviceFilter, DateRangeFilter, RelationshipTypeFilter,
-	 *						MostRecentFilter
+	 * Applicable filters: DeviceFilter, DateRangeFilter,
+	 * RelationshipTypeFilter, MostRecentFilter
 	 *
 	 * @param account1 First AccountDeviceInstance
 	 * @param account2 Second AccountDeviceInstance
@@ -1172,13 +1172,13 @@ public List<Content> getRelationshipSources(AccountDeviceInstance account1, Acco
 				CommunicationsFilter.DeviceFilter.class.getName(),
 				CommunicationsFilter.RelationshipTypeFilter.class.getName()
 		));
-		
+
 		String limitQuery = " account_relationships AS relationships";
 		String limitStr = getMostRecentFilterLimitSQL(filter);
-		if(!limitStr.isEmpty()) {
+		if (!limitStr.isEmpty()) {
 			limitQuery = "(SELECT * FROM account_relationships as relationships " + limitStr + ") as relationships";
 		}
-		
+
 		String filterSQL = getCommunicationsFilterSQL(filter, applicableFilters);
 		final String queryString = "SELECT artifacts.artifact_id AS artifact_id,"
 				+ "		artifacts.obj_id AS obj_id,"
@@ -1187,7 +1187,7 @@ public List<Content> getRelationshipSources(AccountDeviceInstance account1, Acco
 				+ "		artifacts.artifact_type_id AS artifact_type_id,"
 				+ "		artifacts.review_status_id AS review_status_id"
 				+ " FROM blackboard_artifacts AS artifacts"
-				+ "	JOIN " + limitQuery 
+				+ "	JOIN " + limitQuery
 				+ "		ON artifacts.artifact_obj_id = relationships.relationship_source_obj_id"
 				+ " WHERE (( relationships.account1_id = " + account1.getAccount().getAccountID()
 				+ " AND relationships.account2_id  = " + account2.getAccount().getAccountID()
@@ -1220,44 +1220,44 @@ public List<Content> getRelationshipSources(AccountDeviceInstance account1, Acco
 			db.releaseSingleUserCaseReadLock();
 		}
 	}
-	
+
 	/**
 	 * Get a list AccountFileInstance for the given accounts.
-	 * 
+	 *
 	 * @param account List of accounts
-	 * 
-	 * @return	A lit of AccountFileInstances for the given accounts or null if 
-	 *			none are found.
-	 * 
-	 * @throws org.sleuthkit.datamodel.TskCoreException 
+	 *
+	 * @return	A lit of AccountFileInstances for the given accounts or null if
+	 *         none are found.
+	 *
+	 * @throws org.sleuthkit.datamodel.TskCoreException
 	 */
 	public List<AccountFileInstance> getAccountFileInstances(Account account) throws TskCoreException {
 		List<AccountFileInstance> accountFileInstanceList = new ArrayList<>();
-		
+
 		List<BlackboardArtifact> artifactList = getSleuthkitCase().getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ID, account.getTypeSpecificID());
-		
-		if(artifactList != null && !artifactList.isEmpty()) {
-			for(BlackboardArtifact artifact : artifactList) {
+
+		if (artifactList != null && !artifactList.isEmpty()) {
+			for (BlackboardArtifact artifact : artifactList) {
 				accountFileInstanceList.add(new AccountFileInstance(artifact, account));
 			}
 		}
-		
-		if(!accountFileInstanceList.isEmpty()) {
+
+		if (!accountFileInstanceList.isEmpty()) {
 			return accountFileInstanceList;
 		} else {
 			return null;
 		}
 	}
-	
+
 	/**
 	 * Gets a list of the distinct account types that can currently be found in
 	 * the case db.
-	 * 
+	 *
 	 * @return A list of distinct accounts or an empty list.
-	 * 
-	 * @throws TskCoreException 
+	 *
+	 * @throws TskCoreException
 	 */
-	public List<Account.Type> getAccountTypesInUse()  throws TskCoreException{
+	public List<Account.Type> getAccountTypesInUse() throws TskCoreException {
 		CaseDbConnection connection = db.getConnection();
 		db.acquireSingleUserCaseReadLock();
 		Statement s = null;
@@ -1272,12 +1272,12 @@ public List<Account.Type> getAccountTypesInUse()  throws TskCoreException{
 			while (rs.next()) {
 				String accountTypeName = rs.getString("type_name");
 				accountType = this.typeNameToAccountTypeMap.get(accountTypeName);
-				
-				if(accountType == null) {
+
+				if (accountType == null) {
 					accountType = new Account.Type(accountTypeName, rs.getString("display_name"));
 					this.accountTypeToTypeIdMap.put(accountType, rs.getInt("account_type_id"));
 				}
-				
+
 				inUseAccounts.add(accountType);
 			}
 			return inUseAccounts;
@@ -1341,8 +1341,8 @@ private String normalizePhoneNum(String phoneNum) {
 		if (phoneNum.startsWith("+")) {
 			normailzedPhoneNum = "+" + normailzedPhoneNum;
 		}
-		
-		if(normailzedPhoneNum.isEmpty()) {
+
+		if (normailzedPhoneNum.isEmpty()) {
 			normailzedPhoneNum = phoneNum;
 		}
 
@@ -1405,27 +1405,28 @@ private String getCommunicationsFilterSQL(CommunicationsFilter commFilter, Set<S
 		}
 		return sqlStr;
 	}
-	
+
 	/**
 	 * Builds the SQL for the MostRecentFilter.
-	 * 
+	 *
 	 * @param filter	The CommunicationsFilter to get the SQL for.
-	 * @return			Order BY and LIMIT clause or empty 
-	 *					string if no filter is available.
+	 *
+	 * @return	Order BY and LIMIT clause or empty string if no filter is
+	 *         available.
 	 */
 	private String getMostRecentFilterLimitSQL(CommunicationsFilter filter) {
 		String limitStr = "";
-		
+
 		if (filter != null && !filter.getAndFilters().isEmpty()) {
 
 			for (CommunicationsFilter.SubFilter subFilter : filter.getAndFilters()) {
-				if(subFilter.getClass().getName().equals(CommunicationsFilter.MostRecentFilter.class.getName())) {
-					limitStr =  subFilter.getSQL(this);
+				if (subFilter.getClass().getName().equals(CommunicationsFilter.MostRecentFilter.class.getName())) {
+					limitStr = subFilter.getSQL(this);
 					break;
 				}
 			}
 		}
-		
+
 		return limitStr;
 	}
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 0bae69b9c46d01dbad0208aaba3e9ea6266cb0ea..a8e66aa4b98b2800e87d78de353290531b2ba6ba 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -10710,6 +10710,7 @@ public void deleteReport(Report report) throws TskCoreException {
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error querying reports table", ex);
 		} finally {
+			connection.close();
 			releaseSingleUserCaseWriteLock();
 		}
 	}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
index 2d062e02b5d5172652f64e8403222d50613d652a..1fb7cd76292126b94ace0eb621e79f904a04ded4 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
@@ -21,13 +21,12 @@
 import com.google.common.net.InternetDomainName;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS;
-import org.sleuthkit.datamodel.blackboardutils.attributes.GeoTrackPoints;
-import org.sleuthkit.datamodel.blackboardutils.attributes.GeoWaypoint.GeoTrackPoint;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil.GeoTrackPointList;
 
 /**
  * Container class for various types of timeline events
@@ -57,6 +56,7 @@ static class URLArtifactEventType extends TimelineEventArtifactTypeSingleDescrip
 			super(typeID, displayName, superType, artifactType, timeAttribute, descriptionAttribute);
 		}
 
+		@Override
 		TimelineEventDescription parseDescription(String fullDescriptionRaw, String medDescriptionRaw, String shortDescriptionRaw) {
 			/**
 			 * Parses the full description from db, which is the full URL, to a
@@ -99,6 +99,7 @@ static class FilePathEventType extends TimelineEventTypeImpl {
 			super(typeID, displayName, eventTypeZoomLevel, superType);
 		}
 
+		@Override
 		TimelineEventDescription parseDescription(String fullDescription, String medDescription, String shortDescription) {
 			return parseFilePathDescription(fullDescription);
 		}
@@ -111,6 +112,7 @@ static class FilePathArtifactEventType extends TimelineEventArtifactTypeSingleDe
 			super(typeID, displayName, superType, artifactType, timeAttribute, descriptionAttribute);
 		}
 
+		@Override
 		TimelineEventDescription parseDescription(String fullDescriptionRaw, String medDescriptionRaw, String shortDescriptionRaw) {
 			return parseFilePathDescription(fullDescriptionRaw);
 		}
@@ -123,6 +125,8 @@ TimelineEventDescription parseDescription(String fullDescriptionRaw, String medD
 	 */
 	static class GPSTrackArtifactEventType extends TimelineEventArtifactTypeSingleDescription {
 		
+		private final TskGeoTrackpointsUtil trackpointUtil = new TskGeoTrackpointsUtil();
+		
 		GPSTrackArtifactEventType(int typeID, String displayName, TimelineEventType superType, BlackboardArtifact.Type artifactType, BlackboardAttribute.Type descriptionAttribute) {
 			// Passing TSK_GEO_TRACKPOINTS as the "time attribute" as more of a place filler, to avoid any null issues
 			super(typeID, displayName, superType, artifactType, new BlackboardAttribute.Type(TSK_GEO_TRACKPOINTS), descriptionAttribute);
@@ -138,15 +142,8 @@ public TimelineEventDescriptionWithTime makeEventDescription(BlackboardArtifact
 			}
 			
 			// Get the waypoint list "start time"
-			List<GeoTrackPoint> points = GeoTrackPoints.deserializePoints(attribute.getValueString());
-			Long startTime = null;
-			for (GeoTrackPoint point : points) {
-				// Points are in time order so return the first non-null time stamp
-				startTime = point.getTimeStamp();
-				if (startTime != null) {
-					break;
-				}
-			}
+			GeoTrackPointList pointsList = trackpointUtil.fromAttribute(attribute);
+			Long startTime = pointsList.getStartTime();
 			
 			// If we didn't find a startime do not create an event.
 			if (startTime == null) {
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
index 89af4887ff33a91ae54818b7bdef802c2aac6b2a..0e3f99f5a072d33678e470d91dfb72af8b68e6fe 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
@@ -18,7 +18,8 @@
  */
 package org.sleuthkit.datamodel.blackboardutils;
 
-import org.sleuthkit.datamodel.blackboardutils.attributes.GeoTrackPoints;
+import java.util.ArrayList;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil.GeoTrackPointList;
 import java.util.List;
 import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
@@ -26,7 +27,9 @@
 import org.sleuthkit.datamodel.Content;
 import org.sleuthkit.datamodel.SleuthkitCase;
 import org.sleuthkit.datamodel.TskCoreException;
-import org.sleuthkit.datamodel.blackboardutils.attributes.GeoWaypoint.GeoTrackPoint;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoWaypointsUtil.GeoWaypointList;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoWaypointsUtil;
 
 /**
  * Class to help ingest modules create Geolocation artifacts.
@@ -34,44 +37,112 @@
  */
 public final class GeoArtifactsHelper extends ArtifactHelperBase {
 
+	private final String programName;
+	private final TskGeoTrackpointsUtil trackPointAttributeUtil;
+	private final TskGeoWaypointsUtil waypointsAttributeUtil;
+
 	/**
 	 * Constructs a geolocation artifact helper for the given source file.
 	 *
-	 * @param caseDb			  Sleuthkit case db.
+	 * @param caseDb		Sleuthkit case db.
 	 * @param moduleName	Name of module using the helper.
-	 * @param srcFile			 Source file being processed by the module.
+	 * @param programName	Optional program name for TSK_PROG_NAME attribute, 
+	 *						nulls and empty string will be ignored.
+	 * @param srcFile		Source file being processed by the module.
 	 */
-	public GeoArtifactsHelper(SleuthkitCase caseDb, String moduleName, Content srcFile) {
+	public GeoArtifactsHelper(SleuthkitCase caseDb, String moduleName, String programName, Content srcFile) {
 		super(caseDb, moduleName, srcFile);
+		this.programName = programName;
+		trackPointAttributeUtil = new TskGeoTrackpointsUtil();
+		waypointsAttributeUtil = new TskGeoWaypointsUtil();
 	}
 
 	/**
-	 * Add a Track from a GPS device to the database.  A Track represents a series of points that the device
-	 * has traveled on.  This will create a TSK_GPS_TRACK artifact and add it to the case. 
+	 * Add a Track from a GPS device to the database. A Track represents a 
+	 * series of points that the device has traveled on. This will create a 
+	 * TSK_GPS_TRACK artifact and add it to the case.
 	 *
-	 * @param trackName	Name of GPS track, not required.  Pass in null if unknown. 
-	 * @param points	Set of GeoTrackPoints that the track traversed. Required.
+	 * @param trackName			Name of GPS track, not required.
+	 * @param points			List of GeoTrackPoints that the track traversed.
+	 *							Required.
+	 * @param moreAttributes	Optional list of other artifact attributes
 	 *
 	 * @return	TSK_GPS_TRACK artifact
 	 *
-	 * @throws TskCoreException		  If there is an error creating the artifact.
+	 * @throws TskCoreException		If there is an error creating the artifact.
 	 * @throws BlackboardException	If there is a problem posting the artifact
 	 */
-	public BlackboardArtifact addTrack(String trackName, List<GeoTrackPoint> points) throws TskCoreException, BlackboardException {
-		if (points == null) {
-			throw new IllegalArgumentException("GeoTrackPoint instance must be valid");
+	public BlackboardArtifact addTrack(String trackName, GeoTrackPointList points, List<BlackboardAttribute> moreAttributes) throws TskCoreException, BlackboardException {
+		
+		if(points == null) {
+			throw new IllegalArgumentException(String.format("addTrack was passed a null list of track points"));
 		}
-
+		
 		BlackboardArtifact artifact = getContent().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACK);
+		List<BlackboardAttribute> attributes = new ArrayList<>();
 		if (trackName != null) {
-			artifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, getModuleName(), trackName));
+			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, getModuleName(), trackName));
+		}
+
+		attributes.add(trackPointAttributeUtil.toAttribute(getModuleName(), points));
+
+		if (programName != null) {
+			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME, getModuleName(), programName));
+		}
+
+		if (moreAttributes != null) {
+			attributes.addAll(moreAttributes);
+		}
+		
+		artifact.addAttributes(attributes);
+
+		getSleuthkitCase().getBlackboard().postArtifact(artifact, getModuleName());
+
+		return artifact;
+	}
+
+	/**
+	 * Add a Route from a GPS device to the database. This will create a 
+	 * TSK_GPS_ROUTE artifact and add it to the case.
+	 *
+	 * @param routeName			Optional route name
+	 * @param creationTime		Time the route was created, optional.
+	 * @param points			List of GeoWaypointList belonging to the route, required
+	 * @param moreAttributes	Optional list of other artifact attributes.
+	 *
+	 * @return TSK_GPS_ROUTE artifact
+	 *
+	 * @throws TskCoreException		If there is an error creating the artifact.
+	 * @throws BlackboardException	If there is a problem posting the artifact.
+	 */
+	public BlackboardArtifact addRoute(String routeName, Long creationTime, GeoWaypointList points, List<BlackboardAttribute> moreAttributes) throws TskCoreException, BlackboardException {
+
+		if (points == null) {
+			throw new IllegalArgumentException(String.format("addRoute was passed a null list of waypoints"));
+		}
+
+		BlackboardArtifact artifact = getContent().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE);
+		List<BlackboardAttribute> attributes = new ArrayList<>();
+
+		attributes.add(waypointsAttributeUtil.toAttribute(getModuleName(), points));
+		
+		if (routeName != null) {
+			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, getModuleName(), routeName));
+		}
+
+		if (creationTime != null) {
+			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, getModuleName(), creationTime));
+		}
+
+		if (programName != null) {
+			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME, getModuleName(), programName));
+		}
+
+		if (moreAttributes != null) {
+			attributes.addAll(moreAttributes);
 		}
 
-		artifact.addAttribute(
-				new BlackboardAttribute(
-						BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS,
-						getModuleName(),
-						GeoTrackPoints.serializePoints(points)));
+		artifact.addAttributes(attributes);
 
 		getSleuthkitCase().getBlackboard().postArtifact(artifact, getModuleName());
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
index 40e3177e4649aa517609b7f233b608b98235ac64..1a4cb5f2cb808720b6fcefaac49efa1a59fdbd2b 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
@@ -1,7 +1,7 @@
 /*
  * Sleuth Kit Data Model
  *
- * Copyright 2019 Basis Technology Corp.
+ * Copyright 2019-2020 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,10 +25,11 @@
 import java.util.Collections;
 import java.util.StringTokenizer;
 import org.apache.commons.lang3.StringUtils;
-import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.Account;
 import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
 import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.CommunicationsManager;
 import org.sleuthkit.datamodel.Content;
 import org.sleuthkit.datamodel.SleuthkitCase;
 import org.sleuthkit.datamodel.TskCoreException;
@@ -296,6 +297,15 @@ public BlackboardArtifact addWebFormAddress(String personName, String email,
 
 		BlackboardArtifact webFormAddressArtifact;
 		Collection<BlackboardAttribute> attributes = new ArrayList<>();
+		
+		CommunicationsManager commManager = this.getSleuthkitCase().getCommunicationsManager();
+		if(StringUtils.isNotBlank(email)) {
+			commManager.createAccountFileInstance(Account.Type.EMAIL, email, this.getModuleName(), this.getContent());
+		}
+
+		if(StringUtils.isNotBlank(phoneNumber)) {
+			commManager.createAccountFileInstance(Account.Type.PHONE, phoneNumber, this.getModuleName(), this.getContent());
+		}
 
 		// create artifact
 		webFormAddressArtifact = getContent().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_FORM_ADDRESS);
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardAttributeUtil.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardAttributeUtil.java
new file mode 100755
index 0000000000000000000000000000000000000000..d32b264a56a6309498990d944c4125089b9bb48a
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardAttributeUtil.java
@@ -0,0 +1,48 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *	 http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel.blackboardutils.attributes;
+
+import org.sleuthkit.datamodel.BlackboardAttribute;
+
+/**
+ * An interface for Utility classes to implement for translating
+ * BlackboardAttributes to and from a particular format. Initial use case is for
+ * BlackboardAttributes of type TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON.
+ */
+public interface BlackboardAttributeUtil<T> {
+
+	/**
+	 * Translates the value of type T to a attribute.
+	 *
+	 * @param moduleName	Name of module creating the artifact
+	 * @param value			Object to Translate to attribute
+	 *
+	 * @return BlackboardAttribute created from value
+	 */
+	BlackboardAttribute toAttribute(String moduleName, T value);
+
+	/**
+	 * Translates a attribute to an object of type T.
+	 *
+	 * @param attribute The attribute to be translated to T
+	 *
+	 * @return A new instance of T created from the attribute
+	 */
+	T fromAttribute(BlackboardAttribute attribute);
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoTrackPoints.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoTrackPoints.java
deleted file mode 100755
index 21474995e57360bf5102f1283424ec6ec86bcdfc..0000000000000000000000000000000000000000
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoTrackPoints.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Sleuth Kit Data Model
- *
- * Copyright 2020 Basis Technology Corp.
- * Contact: carrier <at> sleuthkit <dot> org
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *	 http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.sleuthkit.datamodel.blackboardutils.attributes;
-
-import com.google.gson.Gson;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.sleuthkit.datamodel.blackboardutils.attributes.GeoWaypoint.GeoTrackPoint;
-
-/**
- * Helper class to make it easier to serialize and deserialize the list of track
- * points with Gson.
- *
- */
-public final class GeoTrackPoints {
-
-	private final List<GeoTrackPoint> points;
-
-	/**
-	 * Deserialize the given list of GeoTrackPoints.
-	 *
-	 * @param jsonString JSon string of track points.
-	 *
-	 * @return	Timestamp ordered list of GeoTrackPoints, empty list will be
-	 *         returned if jsonString is null or empty.
-	 */
-	public static List<GeoTrackPoint> deserializePoints(String jsonString) {
-		if (jsonString == null || jsonString.isEmpty()) {
-			return new ArrayList<>();
-		}
-
-		GeoTrackPoints trackPoints = (new Gson()).fromJson(jsonString, GeoTrackPoints.class);
-		return trackPoints.getTimeOrderedPoints();
-	}
-
-	/**
-	 * Serialize the given list of GeoTrackPoints.
-	 *
-	 * @param points List of GeoTrackPoints
-	 *
-	 * @return	JSon formatted string is returned or empty string if points was
-	 *         null
-	 */
-	public static String serializePoints(List<GeoTrackPoint> points) {
-		if (points == null) {
-			return "";
-		}
-
-		Gson gson = new Gson();
-		return gson.toJson(new GeoTrackPoints(points));
-	}
-
-	/**
-	 * Constructs a new instance with the give list of GeoTrackPoints.
-	 *
-	 * @param points
-	 */
-	private GeoTrackPoints(List<GeoTrackPoint> points) {
-		if (points == null) {
-			throw new IllegalArgumentException("Invalid list of track points passed to constructor");
-		}
-
-		this.points = points;
-	}
-
-	/**
-	 * Returns a timestamp ordered copy of the points list.
-	 *
-	 * @return timestamp
-	 */
-	private List<GeoTrackPoint> getTimeOrderedPoints() {
-		return points.stream().sorted().collect(Collectors.toCollection(ArrayList::new));
-	}
-}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoWaypoint.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoWaypoint.java
deleted file mode 100755
index 14fb0358d991a7a3c495c849a02361337d9d6bde..0000000000000000000000000000000000000000
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoWaypoint.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Sleuth Kit Data Model
- *
- * Copyright 2020 Basis Technology Corp.
- * Contact: carrier <at> sleuthkit <dot> org
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *	 http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.sleuthkit.datamodel.blackboardutils.attributes;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * Class that represents a single waypoint made up of longitude, latitude, and
- * altitude.
- */
-public class GeoWaypoint {
-
-	@SerializedName("TSK_GEO_LATITUDE")
-	private final Double latitude;
-	@SerializedName("TSK_GEO_LONGITUDE")
-	private final Double longitude;
-	@SerializedName("TSK_GEO_ALTITUDE")
-	private final Double altitude;
-
-	/**
-	 * Creates a GeoWaypoint instance.
-	 *
-	 * @param latitude  The latitude, required
-	 * @param longitude The longitude, required
-	 * @param altitude  The altitude, can be null
-	 */
-	public GeoWaypoint(Double latitude, Double longitude, Double altitude) {
-		if (latitude == null || longitude == null) {
-			throw new IllegalArgumentException("Null cordinate value passed to waypoint constructor");
-		}
-
-		this.latitude = latitude;
-		this.longitude = longitude;
-		this.altitude = altitude;
-	}
-
-	/**
-	 * Returns latitude of the waypoint.
-	 *
-	 * @return Double latitude value
-	 */
-	public Double getLatitude() {
-		return latitude;
-	}
-
-	/**
-	 * Returns longitude of the waypoint.
-	 *
-	 * @return Double longitude value
-	 */
-	public Double getLongitude() {
-		return longitude;
-	}
-
-	/**
-	 * Get the altitude if available for this waypoint.
-	 *
-	 * @return Double altitude value, maybe null if not available or applicable
-	 */
-	public Double getAltitude() {
-		return altitude;
-	}
-
-	/**
-	 * A GeoTrackPoint is a Waypoint with more detailed information about the
-	 * point.
-	 *
-	 */
-	public final static class GeoTrackPoint extends GeoWaypoint implements Comparable<GeoTrackPoint> {
-
-		@SerializedName("TSK_GEO_VELOCITY")
-		private final Double velocity;
-		@SerializedName("TSK_GEO_DISTANCE_FROM_HOME_POINT")
-		private final Double distanceFromHP;
-		@SerializedName("TSK_GEO_DISTANCE_TRAVELED")
-		private final Double distanceTraveled;
-		@SerializedName("TSK_DATETIME")
-		private final Long timestamp;
-
-		/**
-		 * Constructs a GeoTrackPoint with the given attributes.
-		 *
-		 * @param latitude			      Latitude of the trackpoint, required
-		 * @param longitude			     Longitude of the trackpoint, required
-		 * @param altitude			      Altitude of the trackpoint, maybe null
-		 * @param velocity			      Velocity in meters/sec, maybe null
-		 * @param distanceFromHP	  Trackpoint distance from an established "home
-		 *                         point", maybe null if not applicable
-		 * @param distanceTraveled	Overall distance traveled in meters at the
-		 *                         time this trackpoint was created, maybe null
-		 *                         if not applicable
-		 * @param timestamp			     Trackpoint creation time, maybe null if not
-		 *                         applicable
-		 */
-		public GeoTrackPoint(Double latitude,
-				Double longitude,
-				Double altitude,
-				Double velocity,
-				Double distanceFromHP,
-				Double distanceTraveled,
-				Long timestamp) {
-			super(latitude, longitude, altitude);
-			this.velocity = velocity;
-			this.distanceFromHP = distanceFromHP;
-			this.distanceTraveled = distanceTraveled;
-			this.timestamp = timestamp;
-		}
-
-		/**
-		 * Returns velocity of the point.
-		 *
-		 * @return Double velocity value, maybe null if not available or
-		 *         applicable
-		 */
-		public Double getVelocity() {
-			return velocity;
-		}
-
-		/**
-		 * Returns distance from home point for the point.
-		 *
-		 * @return Double velocity distance from home point, maybe null if not
-		 *         available or applicable
-		 */
-		public Double getDistanceFromHP() {
-			return distanceFromHP;
-		}
-
-		/**
-		 * Returns distance traveled for the point.
-		 *
-		 * @return Double distance traveled value, maybe null if not available
-		 *         or applicable
-		 */
-		public Double getDistanceTraveled() {
-			return distanceTraveled;
-		}
-
-		/**
-		 * Returns the time stamp (seconds from java/unix epoch) of the track
-		 * point.
-		 *
-		 * @return time stamp of the track point, or null if not available
-		 */
-		public Long getTimeStamp() {
-			return timestamp;
-		}
-
-		@Override
-		public int compareTo(GeoTrackPoint otherTP) {
-			Long otherTimeStamp = otherTP.getTimeStamp();
-
-			if (timestamp == null && otherTimeStamp != null) {
-				return -1;
-			} else if (timestamp != null && otherTimeStamp == null) {
-				return 1;
-			} else {
-				return timestamp.compareTo(otherTP.getTimeStamp());
-			}
-		}
-	}
-
-}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoTrackpointsUtil.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoTrackpointsUtil.java
new file mode 100755
index 0000000000000000000000000000000000000000..ad434f5c7d3e1da86cbf7620629b9fb0a9c9b835
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoTrackpointsUtil.java
@@ -0,0 +1,330 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *	 http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel.blackboardutils.attributes;
+
+import com.google.gson.Gson;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil.GeoTrackPointList;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil.GeoTrackPointList.GeoTrackPoint;
+
+/**
+ * Utility class for translating TSK_GEO_TRACKPOINTS attribute values to
+ * GeoTrackPointList objects and GeoTrackPointList to BlackboardAttributes.
+ */
+public final class TskGeoTrackpointsUtil implements BlackboardAttributeUtil<GeoTrackPointList> {
+
+	@Override
+	public BlackboardAttribute toAttribute(String moduleName, GeoTrackPointList value) {
+
+		if (value == null) {
+			throw new IllegalArgumentException("toAttribute was passed a null list");
+		}
+
+		return new BlackboardAttribute(
+				BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS,
+				moduleName,
+				toJSON(value));
+	}
+
+	@Override
+	public GeoTrackPointList fromAttribute(BlackboardAttribute attribute) {
+		if (attribute == null) {
+			throw new IllegalArgumentException("fromAttribute was passed a null attribute");
+		}
+
+		BlackboardAttribute.ATTRIBUTE_TYPE type = BlackboardAttribute.ATTRIBUTE_TYPE.fromID(attribute.getAttributeType().getTypeID());
+		if (type != BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS) {
+			throw new IllegalArgumentException(String.format("Invalid attribute of type %s passed to fromAttribute method. Attribute of type TSK_GEO_TRACKPOINTS is required", type.getDisplayName()));
+		}
+
+		return fromJSON(attribute.getValueString());
+	}
+
+	/**
+	 * Creates a GeoTrackPointList from the given JSON string.
+	 *
+	 * @param jsonString JSon string of track points.
+	 *
+	 * @return	Timestamp ordered list of GeoTrackPoints, empty list will be
+	 *			returned if jsonString is null or empty.
+	 */
+	private static GeoTrackPointList fromJSON(String jsonString) {
+		if (jsonString == null || jsonString.isEmpty()) {
+			return null;
+		}
+
+		return (new Gson()).fromJson(jsonString, GeoTrackPointList.class);
+	}
+
+	/**
+	 * Returns a JSON string representing the given object.
+	 *
+	 * @return JSON string
+	 */
+	private static String toJSON(GeoTrackPointList pointList) {
+		Gson gson = new Gson();
+		return gson.toJson(pointList);
+	}
+
+	/**
+	 * A list of GeoTrackPoints.
+	 */
+	public static class GeoTrackPointList implements Iterable<GeoTrackPointList.GeoTrackPoint> {
+
+		private final List<GeoTrackPoint> pointList;
+
+		/**
+		 * Construct an empty GeoTrackPointList.
+		 */
+		public GeoTrackPointList() {
+			pointList = new ArrayList<>();
+		}
+
+		/**
+		 * Construct a new instance with the given list of GeoTrackPoint
+		 * objects.
+		 *
+		 * @param points List of track points, cannot be null.
+		 */
+		public GeoTrackPointList(List<GeoTrackPoint> points) {
+			if (points == null) {
+				throw new IllegalArgumentException("Constructor was passed a null list");
+			}
+
+			pointList = points;
+		}
+
+		/**
+		 * Add a point to the list of track points.
+		 *
+		 * @param point A point to add to the track point list, cannot be null.
+		 */
+		public void addPoint(GeoTrackPoint point) {
+			if (point == null) {
+				throw new IllegalArgumentException("addPoint was passed a null list");
+			}
+
+			pointList.add(point);
+		}
+
+		/**
+		 * Adds a new point with the given attributes.
+		 *
+		 * @param latitude			    Latitude of the trackpoint, required
+		 * @param longitude			    Longitude of the trackpoint, required
+		 * @param altitude			    Altitude of the trackpoint, maybe null
+		 * @param name				    Name of trackpoint, maybe null
+		 * @param velocity				Velocity in meters/sec, maybe null
+		 * @param distanceFromHomePoint	Track point distance from an established
+		 *                              "home point", may be null if not
+		 *                              applicable
+		 * @param distanceTraveled			   Overall distance traveled in meters at
+		 *                              the time this trackpoint was created,
+		 *                              maybe null if not applicable
+		 * @param timestamp					        Trackpoint creation time, maybe null if
+		 *                              not applicable
+		 */
+		public void addPoint(Double latitude,
+				Double longitude,
+				Double altitude,
+				String name,
+				Double velocity,
+				Double distanceFromHomePoint,
+				Double distanceTraveled,
+				Long timestamp) {
+			pointList.add(new GeoTrackPoint(
+					latitude,
+					longitude,
+					altitude,
+					name,
+					velocity,
+					distanceFromHomePoint,
+					distanceTraveled,
+					timestamp));
+		}
+
+		/**
+		 * Returns an iterator over the points in this GeoTrackPointList.
+		 *
+		 * @return An iterator over the elements of the list.
+		 */
+		@Override
+		public Iterator<GeoTrackPoint> iterator() {
+			return pointList.iterator();
+		}
+
+		/**
+		 * Returns true if this list contains no points.
+		 *
+		 * @return True if this list contains no points.
+		 */
+		public boolean isEmpty() {
+			return pointList.isEmpty();
+		}
+
+		/**
+		 * Return the start time for the track.
+		 *
+		 * @return First non-null time stamp or null, if one was not found.
+		 */
+		public Long getStartTime() {
+			List<GeoTrackPoint> orderedPoints = getTimeOrderedPoints();
+			if (orderedPoints != null) {
+				for (GeoTrackPoint point : orderedPoints) {
+					if (point.getTimeStamp() != null) {
+						return point.getTimeStamp();
+					}
+				}
+			}
+			return null;
+		}
+
+		/**
+		 * Return the ends time for the track.
+		 *
+		 * @return First non-null time stamp or null, if one was not found.
+		 */
+		public Long getEndTime() {
+			List<GeoTrackPoint> orderedPoints = getTimeOrderedPoints();
+			if (orderedPoints != null) {
+				for (int index = orderedPoints.size() - 1; index >= 0; index--) {
+					GeoTrackPoint point = orderedPoints.get(index);
+					if (point.getTimeStamp() != null) {
+						return point.getTimeStamp();
+					}
+				}
+			}
+			return null;
+		}
+
+		/**
+		 * Returns a timestamp ordered copy of the points list.
+		 *
+		 * @return List of points sorted by timestamps.
+		 */
+		private List<GeoTrackPoint> getTimeOrderedPoints() {
+			return pointList.stream().sorted().collect(Collectors.toCollection(ArrayList::new));
+		}
+
+		/**
+		 * A GeoTrackPoint is a Waypoint with more detailed information about
+		 * the point.
+		 *
+		 */
+		public final static class GeoTrackPoint extends TskGeoWaypointsUtil.GeoWaypointList.GeoWaypoint implements Comparable<GeoTrackPointList.GeoTrackPoint> {
+
+			private final Double velocity;
+			private final Double distanceFromHomePoint;
+			private final Double distanceTraveled;
+			private final Long timestamp;
+
+			/**
+			 * Constructs a GeoTrackPoint with the given attributes.
+			 *
+			 * @param latitude				Latitude of the track point, required
+			 * @param longitude				Longitude of the track point,
+			 *                              required
+			 * @param altitude			    Altitude of the track point, may be
+			 *                              null
+			 * @param name				    Name of track point, may be null
+			 * @param velocity			    Velocity in meters/sec, may be null
+			 * @param distanceFromHomePoint	Track point distance from an
+			 *                              established "home point", maybe null
+			 *                              if not applicable
+			 * @param distanceTraveled	    Overall distance traveled in meters
+			 *                              at the time this track point was
+			 *                              created, maybe null if not
+			 *                              applicable
+			 * @param timestamp			    Track point creation time, maybe
+			 *                              null if not applicable
+			 */
+			public GeoTrackPoint(Double latitude,
+					Double longitude,
+					Double altitude,
+					String name,
+					Double velocity,
+					Double distanceFromHomePoint,
+					Double distanceTraveled,
+					Long timestamp) {
+				super(latitude, longitude, altitude, name);
+				this.velocity = velocity;
+				this.distanceFromHomePoint = distanceFromHomePoint;
+				this.distanceTraveled = distanceTraveled;
+				this.timestamp = timestamp;
+			}
+
+			/**
+			 * Returns velocity of the point.
+			 *
+			 * @return Double velocity value, maybe null if not available or
+			 *         applicable
+			 */
+			public Double getVelocity() {
+				return velocity;
+			}
+
+			/**
+			 * Returns distance from home point for the point.
+			 *
+			 * @return Double velocity distance from home point, maybe null if
+			 *         not available or applicable
+			 */
+			public Double getDistanceFromHomePoint() {
+				return distanceFromHomePoint;
+			}
+
+			/**
+			 * Returns distance traveled for the point.
+			 *
+			 * @return Double distance traveled value, maybe null if not
+			 *         available or applicable
+			 */
+			public Double getDistanceTraveled() {
+				return distanceTraveled;
+			}
+
+			/**
+			 * Returns the time stamp (seconds from java/unix epoch) of the
+			 * track point.
+			 *
+			 * @return time stamp of the track point, or null if not available
+			 */
+			public Long getTimeStamp() {
+				return timestamp;
+			}
+
+			@Override
+			public int compareTo(GeoTrackPointList.GeoTrackPoint otherTP) {
+				Long otherTimeStamp = otherTP.getTimeStamp();
+
+				if (timestamp == null && otherTimeStamp != null) {
+					return -1;
+				} else if (timestamp != null && otherTimeStamp == null) {
+					return 1;
+				} else {
+					return timestamp.compareTo(otherTP.getTimeStamp());
+				}
+			}
+		}
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoWaypointsUtil.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoWaypointsUtil.java
new file mode 100755
index 0000000000000000000000000000000000000000..08d5a1c6bd6a77ab67bf0992c5a73a2d4e58ebe5
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoWaypointsUtil.java
@@ -0,0 +1,197 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *	 http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel.blackboardutils.attributes;
+
+import com.google.gson.Gson;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoWaypointsUtil.GeoWaypointList;
+import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoWaypointsUtil.GeoWaypointList.GeoWaypoint;
+
+/**
+ * Utility class for Translating TSK_GEO_WAYPOINTS attribute values to
+ * GeoWaypointList objects and GeoWaypointList to BlackboardAttributes.
+ */
+public final class TskGeoWaypointsUtil implements BlackboardAttributeUtil<GeoWaypointList> {
+
+	@Override
+	public BlackboardAttribute toAttribute(String moduleName, GeoWaypointList value) {
+
+		if (value == null) {
+			throw new IllegalArgumentException("toAttribute was pass a null list");
+		}
+
+		return new BlackboardAttribute(
+				BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_WAYPOINTS,
+				moduleName,
+				toJSON(value));
+	}
+
+	@Override
+	public GeoWaypointList fromAttribute(BlackboardAttribute attribute) {
+		if (attribute == null) {
+			throw new IllegalArgumentException("fromAttribute was pass a null list");
+		}
+
+		BlackboardAttribute.ATTRIBUTE_TYPE type = BlackboardAttribute.ATTRIBUTE_TYPE.fromID(attribute.getAttributeType().getTypeID());
+		if (type != BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_WAYPOINTS) {
+			throw new IllegalArgumentException(String.format("Invalid attribute of type %s passed to fromAttribute method. Attribute of type TSK_GEO_WAYPOINTS is required", type.getDisplayName()));
+		}
+
+		return fromJSON(attribute.getValueString());
+	}
+
+	/**
+	 * Deserialize the given list of GeoTrackPoints.
+	 *
+	 * @param jsonString JSon string of track points.
+	 *
+	 * @return	Timestamp ordered list of GeoTrackPoints, empty list will be
+	 *         returned if jsonString is null or empty.
+	 */
+	private static GeoWaypointList fromJSON(String jsonString) {
+		if (jsonString == null || jsonString.isEmpty()) {
+			return null;
+		}
+
+		return (new Gson()).fromJson(jsonString, GeoWaypointList.class);
+	}
+
+	/**
+	 * Returns a JSON string can than be used as the TSK_GEO_TRACKPOINTS
+	 * attribute of the TSK_GPS_TRACK artifact.
+	 *
+	 * @return JSON string
+	 */
+	private static String toJSON(GeoWaypointList pointList) {
+		Gson gson = new Gson();
+		return gson.toJson(pointList);
+	}
+
+	/**
+	 * Helper class to make it easier to serialize and deserialize the list of
+	 * waypoints points with json.
+	 *
+	 */
+	public static final class GeoWaypointList implements Iterable<GeoWaypointList.GeoWaypoint> {
+
+		private final List<GeoWaypoint> points;
+
+		public GeoWaypointList() {
+			points = new ArrayList<>();
+		}
+
+		/**
+		 * Adds a point to the list of waypoints.
+		 *
+		 * @param latitude  The latitude, required
+		 * @param longitude The longitude, required
+		 * @param altitude  The altitude, can be null
+		 * @param name		A name for the point, can be null
+		 */
+		public void addPoint(Double latitude, Double longitude, Double altitude, String name) {
+			points.add(new GeoWaypoint(latitude, longitude, altitude, name));
+		}
+
+		/**
+		 * Returns true if this list contains no points.
+		 *
+		 * @return True if this list contains no points.
+		 */
+		public boolean isEmpty() {
+			return points.isEmpty();
+		}
+
+		@Override
+		public Iterator<GeoWaypointList.GeoWaypoint> iterator() {
+			return points.iterator();
+		}
+
+		/**
+		 * Class that represents a single waypoint made up of longitude,
+		 * latitude, and altitude.
+		 */
+		public static class GeoWaypoint {
+
+			private final Double latitude;
+			private final Double longitude;
+			private final Double altitude;
+			private final String name;
+
+			/**
+			 * Creates a GeoWaypoint instance.
+			 *
+			 * @param latitude  The latitude, required
+			 * @param longitude The longitude, required
+			 * @param altitude  The altitude, can be null
+			 * @param name		A name for the waypoint, optional
+			 */
+			public GeoWaypoint(Double latitude, Double longitude, Double altitude, String name) {
+				if (latitude == null || longitude == null) {
+					throw new IllegalArgumentException("Constructor was passed null coordinate");
+				}
+
+				this.latitude = latitude;
+				this.longitude = longitude;
+				this.altitude = altitude;
+				this.name = name;
+			}
+
+			/**
+			 * Returns latitude of the waypoint.
+			 *
+			 * @return Double latitude value
+			 */
+			public Double getLatitude() {
+				return latitude;
+			}
+
+			/**
+			 * Returns longitude of the waypoint.
+			 *
+			 * @return Double longitude value
+			 */
+			public Double getLongitude() {
+				return longitude;
+			}
+
+			/**
+			 * Get the altitude if available for this waypoint.
+			 *
+			 * @return Double altitude value, may be null if not available or
+			 *         applicable
+			 */
+			public Double getAltitude() {
+				return altitude;
+			}
+
+			/**
+			 * Returns the name for this waypoint.
+			 *
+			 * @return	Returns waypoint name, may be null if not available or
+			 *         applicable.
+			 */
+			public String getName() {
+				return name;
+			}
+		}
+	}
+}
diff --git a/tsk/auto/auto.cpp b/tsk/auto/auto.cpp
index 6c08103fb23f4e25cb84cb76eb8653703033c919..fc95317e234d14153a54749a07718e83ba59eff9 100755
--- a/tsk/auto/auto.cpp
+++ b/tsk/auto/auto.cpp
@@ -146,6 +146,10 @@ uint8_t TskAuto::openImageHandle(TSK_IMG_INFO * a_img_info)
 void
  TskAuto::closeImage()
 {
+    for (int i = 0; i < m_poolInfos.size(); i++) {
+        tsk_pool_close(m_poolInfos[i]);
+    }
+
     if ((m_img_info) && (m_internalOpen)) {
         tsk_img_close(m_img_info);
     }
@@ -493,7 +497,10 @@ TskAuto::findFilesInPool(TSK_OFF_T start, TSK_POOL_TYPE_ENUM ptype)
         registerError();
         return TSK_ERR;
     }
-    pool->close(pool);
+
+    // Store the pool_info for later use. It will be closed at the end of the add image process.
+    m_poolInfos.push_back(pool);
+
     return TSK_OK;
 }
 
diff --git a/tsk/auto/auto_db.cpp b/tsk/auto/auto_db.cpp
index 43b84169e186b4603470bb4dbb7f1b41a38e8d00..0fdfdf67090eb6468ae72143481e4f75e3f48afe 100755
--- a/tsk/auto/auto_db.cpp
+++ b/tsk/auto/auto_db.cpp
@@ -315,6 +315,8 @@ TskAutoDb::filterPool(const TSK_POOL_INFO * pool_info)
             registerError();
             return TSK_FILTER_STOP;
         }
+        // Save the parent obj ID for the pool
+        m_poolOffsetToParentId[pool_info->img_offset] = m_curVolId;
     }
     else {
         // pool doesn't live in a volume, use image as parent
@@ -322,13 +324,81 @@ TskAutoDb::filterPool(const TSK_POOL_INFO * pool_info)
             registerError();
             return TSK_FILTER_STOP;
         }
+        // Save the parent obj ID for the pool
+        m_poolOffsetToParentId[pool_info->img_offset] = m_curImgId;
     }
 
-    
+    // Store the volume system object ID for later use
+    m_poolOffsetToVsId[pool_info->img_offset] = m_curPoolVs;
 
     return TSK_FILTER_CONT;
 }
 
+/**
+* Adds unallocated pool blocks to a new volume.
+*
+* @param numPool Will be updated with the number of pools processed
+*
+* @return Returns 0 for success, 1 for failure
+*/
+TSK_RETVAL_ENUM
+TskAutoDb::addUnallocatedPoolBlocksToDb(size_t & numPool) {
+
+    for (int i = 0; i < m_poolInfos.size(); i++) {
+        const TSK_POOL_INFO * pool_info = m_poolInfos[i];
+        if (m_poolOffsetToVsId.find(pool_info->img_offset) == m_poolOffsetToVsId.end()) {
+            tsk_error_reset();
+            tsk_error_set_errno(TSK_ERR_AUTO_DB);
+            tsk_error_set_errstr("Error addUnallocatedPoolBlocksToDb() - could not find volume system object ID for pool at offset %lld", pool_info->img_offset);
+            return TSK_ERR;
+        }
+        int64_t curPoolVs = m_poolOffsetToVsId[pool_info->img_offset];
+
+        /* Make sure  the pool_info is still allocated */
+        if (pool_info->tag != TSK_POOL_INFO_TAG) {
+            tsk_error_reset();
+            tsk_error_set_errno(TSK_ERR_AUTO_DB);
+            tsk_error_set_errstr("Error addUnallocatedPoolBlocksToDb() - pool_info is not allocated");
+            return TSK_ERR;
+        }
+
+        /* Only APFS pools are currently supported */
+        if (pool_info->ctype != TSK_POOL_TYPE_APFS) {
+            continue;
+        }
+
+        /* Increment the count of pools found */
+        numPool++;
+
+        /* Create the volume */
+        int64_t unallocVolObjId;
+        m_db->addUnallocatedPoolVolume(pool_info->num_vols, curPoolVs, unallocVolObjId);
+
+        /* Create the unallocated space files */
+        TSK_FS_ATTR_RUN * unalloc_runs = tsk_pool_unallocated_runs(pool_info);
+        TSK_FS_ATTR_RUN * current_run = unalloc_runs;
+        vector<TSK_DB_FILE_LAYOUT_RANGE> ranges;
+        while (current_run != NULL) {
+
+            TSK_DB_FILE_LAYOUT_RANGE tempRange(current_run->addr * pool_info->block_size, current_run->len * pool_info->block_size, 0);
+
+            ranges.push_back(tempRange);
+            int64_t fileObjId = 0;
+            if (m_db->addUnallocBlockFile(unallocVolObjId, NULL, current_run->len * pool_info->block_size, ranges, fileObjId, m_curImgId)) {
+                registerError();
+                tsk_fs_attr_run_free(unalloc_runs);
+                return TSK_ERR;
+            }
+
+            current_run = current_run->next;
+            ranges.clear();
+        }
+        tsk_fs_attr_run_free(unalloc_runs);
+    }
+
+    return TSK_OK;
+}
+
 TSK_FILTER_ENUM
 TskAutoDb::filterPoolVol(const TSK_POOL_VOLUME_INFO * pool_vol)
 {
@@ -1032,7 +1102,7 @@ TSK_WALK_RET_ENUM TskAutoDb::fsWalkUnallocBlocksCb(const TSK_FS_BLOCK *a_block,
 */
 TSK_RETVAL_ENUM TskAutoDb::addFsInfoUnalloc(const TSK_DB_FS_INFO & dbFsInfo) {
 
-    // Unalloc space is not yet implemented for APFS
+    // Unalloc space is handled separately for APFS
     if (dbFsInfo.fType == TSK_FS_TYPE_APFS) {
         return TSK_OK;
     }
@@ -1103,18 +1173,20 @@ TSK_RETVAL_ENUM TskAutoDb::addUnallocSpaceToDb() {
 
     size_t numVsP = 0;
     size_t numFs = 0;
+    size_t numPool = 0;
 
     TSK_RETVAL_ENUM retFsSpace = addUnallocFsSpaceToDb(numFs);
     TSK_RETVAL_ENUM retVsSpace = addUnallocVsSpaceToDb(numVsP);
+    TSK_RETVAL_ENUM retPoolSpace = addUnallocatedPoolBlocksToDb(numPool);
 
-    //handle case when no fs and no vs partitions
+    //handle case when no fs and no vs partitions or pools
     TSK_RETVAL_ENUM retImgFile = TSK_OK;
-    if (numVsP == 0 && numFs == 0) {
+    if (numVsP == 0 && numFs == 0 && numPool == 0) {
         retImgFile = addUnallocImageSpaceToDb();
     }
     
     
-    if (retFsSpace == TSK_ERR || retVsSpace == TSK_ERR || retImgFile == TSK_ERR)
+    if (retFsSpace == TSK_ERR || retVsSpace == TSK_ERR || retPoolSpace == TSK_ERR || retImgFile == TSK_ERR)
         return TSK_ERR;
     else
         return TSK_OK;
@@ -1217,6 +1289,19 @@ TSK_RETVAL_ENUM TskAutoDb::addUnallocVsSpaceToDb(size_t & numVsP) {
                 //skip processing this vspart
                 continue;
             }
+
+            // Check if the volume contains a pool
+            bool hasPool = false;
+            for (std::map<int64_t, int64_t>::iterator iter = m_poolOffsetToParentId.begin(); iter != m_poolOffsetToParentId.end(); ++iter) {
+                if (iter->second == vsPart.objId) {
+                    hasPool = true;
+                }
+            }
+            if (hasPool) {
+                // Skip processing this vspart
+                continue;
+            }
+
         } //end checking vspart flags
 
         //get sector size and image offset from parent vs info
diff --git a/tsk/auto/db_postgresql.cpp b/tsk/auto/db_postgresql.cpp
index 4afe0e00fa6e847e3ae87dca409565b229ca162a..b7bc126d7bcb8bff0cfb8f7140ac6a74cf621f07 100755
--- a/tsk/auto/db_postgresql.cpp
+++ b/tsk/auto/db_postgresql.cpp
@@ -1012,8 +1012,18 @@ int TskDbPostgreSQL::addImageName(int64_t objId, char const *imgName, int sequen
     return ret;
 }
 
+/**
+* Creates a new tsk_pool_info database entry and a new tsk_vs_info
+* entry with the tsk_pool_info as its parent.
+*
+* @ param pool_info The pool to save to the database
+* @ param parObjId The ID of the parent of the pool object
+* @ param vsObjId Will be set to the object ID of the new volume system created as a child of
+*               the new pool.
+* @returns 1 on error, 0 on success
+*/
 int
-TskDbPostgreSQL::addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& objId) {
+TskDbPostgreSQL::addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& vsObjId) {
 
     char stmt[1024];
 
@@ -1032,11 +1042,11 @@ TskDbPostgreSQL::addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObj
     }
 
     // Add volume system
-    if (addObject(TSK_DB_OBJECT_TYPE_VS, poolObjId, objId))
+    if (addObject(TSK_DB_OBJECT_TYPE_VS, poolObjId, vsObjId))
         return 1;
 
     snprintf(stmt, 1024,
-        "INSERT INTO tsk_vs_info (obj_id, vs_type, img_offset, block_size) VALUES (%" PRId64 ", %d,%" PRIuDADDR ",%d)", objId, TSK_VS_TYPE_APFS, pool_info->img_offset, pool_info->block_size);
+        "INSERT INTO tsk_vs_info (obj_id, vs_type, img_offset, block_size) VALUES (%" PRId64 ", %d,%" PRIuDADDR ",%d)", vsObjId, TSK_VS_TYPE_APFS, pool_info->img_offset, pool_info->block_size);
 
     return attempt_exec(stmt,
         "Error adding data to tsk_vs_info table: %s\n");
@@ -1044,6 +1054,11 @@ TskDbPostgreSQL::addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObj
 
 /**
 * Adds the sector addresses of the pool volumes into the db.
+*
+* @param pool_vol The pool volume to save to the DB
+* @param parObjId The ID of the parent of the pool volume (should be a volume system)
+* @param objId    Will be set to the object ID of the new volume
+*
 * @returns 1 on error, 0 on success
 */
 int
@@ -1073,6 +1088,41 @@ TskDbPostgreSQL::addPoolVolumeInfo(const TSK_POOL_VOLUME_INFO* pool_vol,
     return TSK_OK;
 }
 
+/**
+* Adds a fake volume that will hold the unallocated blocks for the pool.
+*
+* @param vol_index The index for the new volume (should be one higher than the number of pool volumes)
+* @param parObjId  The object ID of the parent volume system
+* @param objId     Will be set to the object ID of the new volume
+*
+* @returns 1 on error, 0 on success
+*/
+int
+TskDbPostgreSQL::addUnallocatedPoolVolume(int vol_index, int64_t parObjId, int64_t& objId)
+{
+
+    char stmt[1024];
+
+    if (addObject(TSK_DB_OBJECT_TYPE_VOL, parObjId, objId))
+        return 1;
+
+    char *desc = "Unallocated Blocks";
+    char *desc_sql = PQescapeLiteral(conn, desc, strlen(desc));
+
+    snprintf(stmt, 1024,
+        "INSERT INTO tsk_vs_parts (obj_id, addr, start, length, descr, flags)"
+        "VALUES (%lld, %" PRIuPNUM ",%d, %d, %s, %d)",
+        objId, vol_index, 0, 0, desc_sql, 0);
+
+    if (attempt_exec(stmt, "Error adding data to tsk_vs_parts table: %s\n")) {
+        PQfreemem(desc_sql);
+        return TSK_ERR;
+    }
+
+    PQfreemem(desc_sql);
+    return TSK_OK;
+}
+
 
 /**
 * @returns 1 on error, 0 on success
diff --git a/tsk/auto/db_sqlite.cpp b/tsk/auto/db_sqlite.cpp
index 2f82aa51bff69cb0af9aec79938993790515de57..2a0e8868f9ef1d5eee4e4726627b772c27ba3655 100644
--- a/tsk/auto/db_sqlite.cpp
+++ b/tsk/auto/db_sqlite.cpp
@@ -754,17 +754,17 @@ TskDbSqlite::addVsInfo(const TSK_VS_INFO* vs_info, int64_t parObjId,
 }
 
 /**
-* Creats a new tsk_pool_info database entry and a new tsk_vs_info 
+* Creates a new tsk_pool_info database entry and a new tsk_vs_info 
 * entry with the tsk_pool_info as its parent.
 *
 * @ param pool_info The pool to save to the database
 * @ param parObjId The ID of the parent of the pool object
-* @ param objId Will be set to the object ID of the new volume system created as a child of 
+* @ param vsObjId Will be set to the object ID of the new volume system created as a child of 
 *               the new pool.
 * @returns 1 on error, 0 on success
 */
 int
-TskDbSqlite::addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& objId) {
+TskDbSqlite::addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& vsObjId) {
 
     char
         stmt[1024];
@@ -784,22 +784,54 @@ TskDbSqlite::addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId,
     }
 
     // Add volume system
-    if (addObject(TSK_DB_OBJECT_TYPE_VS, poolObjId, objId))
+    if (addObject(TSK_DB_OBJECT_TYPE_VS, poolObjId, vsObjId))
         return 1;
 
     snprintf(stmt, 1024,
-        "INSERT INTO tsk_vs_info (obj_id, vs_type, img_offset, block_size) VALUES (%" PRId64 ", %d,%" PRIuDADDR ",%d)", objId, TSK_VS_TYPE_APFS, pool_info->img_offset, pool_info->block_size); // TODO - offset
+        "INSERT INTO tsk_vs_info (obj_id, vs_type, img_offset, block_size) VALUES (%" PRId64 ", %d,%" PRIuDADDR ",%d)", vsObjId, TSK_VS_TYPE_APFS, pool_info->img_offset, pool_info->block_size); // TODO - offset
 
     return attempt_exec(stmt,
         "Error adding data to tsk_vs_info table: %s\n");
 }
 
 /**
-* Adds the sector addresses of the pool volumes into the db.
+* Adds a fake volume that will hold the unallocated blocks for the pool.
+*
+* @param vol_index The index for the new volume (should be one higher than the number of pool volumes)
+* @param parObjId  The object ID of the parent volume system
+* @param objId     Will be set to the object ID of the new volume
+*
+* @returns 1 on error, 0 on success
+*/
+int
+TskDbSqlite::addUnallocatedPoolVolume(int vol_index, int64_t parObjId, int64_t& objId)
+{
+    char* zSQL;
+    int ret;
+
+    if (addObject(TSK_DB_OBJECT_TYPE_VOL, parObjId, objId))
+        return 1;
+
+    char* desc = "Unallocated Blocks";
+    zSQL = sqlite3_mprintf(
+        "INSERT INTO tsk_vs_parts (obj_id, addr, start, length, desc, flags)"
+        "VALUES (%lld, %" PRIuPNUM ",%" PRIuDADDR ",%" PRIuDADDR ",'%q',%d)",
+        objId, vol_index, 0, 0,
+        desc, 0);
 
+    ret = attempt_exec(zSQL,
+        "Error adding data to tsk_vs_parts table: %s\n");
+    sqlite3_free(zSQL);
+    return ret;
+}
+
+/**
+* Adds the sector addresses of the pool volumes into the db.
+*
 * @param pool_vol The pool volume to save to the DB
 * @param parObjId The ID of the parent of the pool volume (should be a volume system)
-* @param objId Will be set to the object ID of the new volume
+* @param objId    Will be set to the object ID of the new volume
+*
 * @returns 1 on error, 0 on success
 */
 int
diff --git a/tsk/auto/tsk_auto.h b/tsk/auto/tsk_auto.h
index 631625614cc14882b82ad74c572af82fa00d4cb7..028a95d82b7a37ced4054ffb3c2ec9bdbd4afe50 100644
--- a/tsk/auto/tsk_auto.h
+++ b/tsk/auto/tsk_auto.h
@@ -263,10 +263,11 @@ class TskAuto {
 
   protected:
     TSK_IMG_INFO * m_img_info;
+    std::vector<const TSK_POOL_INFO*> m_poolInfos;
+
     bool m_internalOpen;        ///< True if m_img_info was opened in TskAuto and false if passed in
     bool m_stopAllProcessing;   ///< True if no further processing should occur
 
-
     uint8_t isNtfsSystemFiles(TSK_FS_FILE * fs_file, const char *path);
     uint8_t isFATSystemFiles(TSK_FS_FILE * fs_file);
     uint8_t isDotDir(TSK_FS_FILE * fs_file);
diff --git a/tsk/auto/tsk_case_db.h b/tsk/auto/tsk_case_db.h
index eee845990c82acf2f44045eb540398e6f53ce6e3..422ca330d66ab1bac894d558d5fc433bb02413e9 100644
--- a/tsk/auto/tsk_case_db.h
+++ b/tsk/auto/tsk_case_db.h
@@ -154,6 +154,12 @@ class TskAutoDb:public TskAuto {
     bool m_foundStructure;  ///< Set to true when we find either a volume or file system
     bool m_attributeAdded; ///< Set to true when an attribute was added by processAttributes
 
+    // These are used to write unallocated blocks for pools at the end of the add image
+    // process. We can't load the pool_info objects directly from the database so we will
+    // store info about them here.
+    std::map<int64_t, int64_t> m_poolOffsetToParentId;
+    std::map<int64_t, int64_t> m_poolOffsetToVsId;
+
     // prevent copying until we add proper logic to handle it
     TskAutoDb(const TskAutoDb&);
     TskAutoDb & operator=(const TskAutoDb&);
@@ -186,7 +192,7 @@ class TskAutoDb:public TskAuto {
         TSK_OFF_T offset, TSK_DADDR_T addr, char *buf, size_t size,
         TSK_FS_BLOCK_FLAG_ENUM a_flags, void *ptr);
     int md5HashAttr(unsigned char md5Hash[16], const TSK_FS_ATTR * fs_attr);
-
+    TSK_RETVAL_ENUM addUnallocatedPoolBlocksToDb(size_t & numPool);
     static TSK_WALK_RET_ENUM fsWalkUnallocBlocksCb(const TSK_FS_BLOCK *a_block, void *a_ptr);
     TSK_RETVAL_ENUM addFsInfoUnalloc(const TSK_DB_FS_INFO & dbFsInfo);
     TSK_RETVAL_ENUM addUnallocFsSpaceToDb(size_t & numFs);
diff --git a/tsk/auto/tsk_db.h b/tsk/auto/tsk_db.h
index 92478fa35c0577c58018fd7263adc45998593f6e..edb300dcee781cb2cb7462f1ca842f3e71507dfc 100755
--- a/tsk/auto/tsk_db.h
+++ b/tsk/auto/tsk_db.h
@@ -180,9 +180,10 @@ class TskDb {
     virtual int addImageName(int64_t objId, char const *imgName, int sequence) = 0;
     virtual int addVsInfo(const TSK_VS_INFO * vs_info, int64_t parObjId, int64_t & objId) = 0;
     virtual int addVolumeInfo(const TSK_VS_PART_INFO * vs_part, int64_t parObjId, int64_t & objId) = 0;
-    virtual int addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& objId) = 0;
+    virtual int addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& vsObjId) = 0;
     virtual int addPoolVolumeInfo(const TSK_POOL_VOLUME_INFO* pool_vol,
         int64_t parObjId, int64_t& objId) = 0;
+    virtual int addUnallocatedPoolVolume(int vol_index, int64_t parObjId, int64_t& objId) = 0;
     virtual int addFsInfo(const TSK_FS_INFO * fs_info, int64_t parObjId, int64_t & objId) = 0;
     virtual int addFsFile(TSK_FS_FILE * fs_file, const TSK_FS_ATTR * fs_attr,
         const char *path, const unsigned char *const md5,
diff --git a/tsk/auto/tsk_db_postgresql.h b/tsk/auto/tsk_db_postgresql.h
index 873b142babca2158d569deeb9c580a089da15d79..c4cfa763158af9f356f828f9e31cb0b6f708debf 100755
--- a/tsk/auto/tsk_db_postgresql.h
+++ b/tsk/auto/tsk_db_postgresql.h
@@ -58,9 +58,10 @@ class TskDbPostgreSQL : public TskDb {
         int64_t & objId);
     int addFsInfo(const TSK_FS_INFO * fs_info, int64_t parObjId,
         int64_t & objId);
-    int addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& objId);
+    int addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& vsObjId);
     int addPoolVolumeInfo(const TSK_POOL_VOLUME_INFO* pool_vol,
         int64_t parObjId, int64_t& objId);
+    int addUnallocatedPoolVolume(int vol_index, int64_t parObjId, int64_t& objId);
     int addFsFile(TSK_FS_FILE * fs_file, const TSK_FS_ATTR * fs_attr,
         const char *path, const unsigned char *const md5,
         const TSK_DB_FILES_KNOWN_ENUM known, int64_t fsObjId,
diff --git a/tsk/auto/tsk_db_sqlite.h b/tsk/auto/tsk_db_sqlite.h
index 7fa3abae9a08da0c338145d9b6ce865a174720e6..13d55412167e0b8e77de117e7c35f0afce86a3ca 100755
--- a/tsk/auto/tsk_db_sqlite.h
+++ b/tsk/auto/tsk_db_sqlite.h
@@ -50,9 +50,10 @@ class TskDbSqlite : public TskDb {
     int addImageName(int64_t objId, char const *imgName, int sequence);
     int addVsInfo(const TSK_VS_INFO * vs_info, int64_t parObjId,
         int64_t & objId);
-    int addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& objId);
+    int addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& vsObjId);
     int addPoolVolumeInfo(const TSK_POOL_VOLUME_INFO* pool_vol,
         int64_t parObjId, int64_t& objId);
+    int addUnallocatedPoolVolume(int vol_index, int64_t parObjId, int64_t& objId);
     int addVolumeInfo(const TSK_VS_PART_INFO * vs_part, int64_t parObjId,
         int64_t & objId);
     int addFsInfo(const TSK_FS_INFO * fs_info, int64_t parObjId,
diff --git a/tsk/fs/fs_open.c b/tsk/fs/fs_open.c
index e1d0cb4a62f194659a3d32c0fb5a2f50887677d6..3a564f0dc5ef2eefd612d7cbfb7d144e6b838c50 100755
--- a/tsk/fs/fs_open.c
+++ b/tsk/fs/fs_open.c
@@ -129,6 +129,8 @@ tsk_fs_open_img_decrypt(TSK_IMG_INFO * a_img_info, TSK_OFF_T a_offset,
         char* name;
         TSK_FS_INFO* (*open)(TSK_IMG_INFO*, TSK_OFF_T,
                                  TSK_FS_TYPE_ENUM, uint8_t);
+        // This type should be the _DETECT version because it used
+        // during autodetection
         TSK_FS_TYPE_ENUM type;
     } FS_OPENERS[] = {
         { "NTFS",     ntfs_open,    TSK_FS_TYPE_NTFS_DETECT    },
diff --git a/tsk/fs/fs_types.c b/tsk/fs/fs_types.c
index 6f656afa84b1308e8993bd7786d3de411680d03e..6ba88f0e6f1e1065fbdedb93fe4b512c4155f334 100644
--- a/tsk/fs/fs_types.c
+++ b/tsk/fs/fs_types.c
@@ -34,6 +34,9 @@ typedef struct {
 /** \internal
  * The table used to parse input strings - supports
  * legacy strings - in order of expected usage
+ *
+ * All unique TSK_FS_TYPE_ENUM values should be in here with a unique 
+ * name so that we can map between values and names. 
  */
 static FS_TYPES fs_type_table[] = {
     {"ntfs", TSK_FS_TYPE_NTFS_DETECT, "NTFS"},
diff --git a/tsk/fs/hfs.c b/tsk/fs/hfs.c
index fede915bdb45bba3648b8a890e2adc7c7fa3c8fd..22618534ee3f0411b88407eae8217a863d0b8eec 100644
--- a/tsk/fs/hfs.c
+++ b/tsk/fs/hfs.c
@@ -835,20 +835,10 @@ hfs_cat_traverse(HFS_INFO * hfs,
                     return 1;
                 }
 
-                if (sizeof(hfs_btree_key_cat) > nodesize - rec_off) {
-                    tsk_error_set_errno(TSK_ERR_FS_GENFS);
-                    tsk_error_set_errstr
-                    ("hfs_cat_traverse: record %d in index node %d truncated",
-                        rec, cur_node);
-                    free(node);
-                    return 1;
-                }
-
                 key = (hfs_btree_key_cat *) & node[rec_off];
-
                 keylen = 2 + tsk_getu16(hfs->fs_info.endian, key->key_len);
-               
-                if (keylen >= nodesize - rec_off) {
+
+                if (keylen > nodesize - rec_off) {
                     tsk_error_set_errno(TSK_ERR_FS_GENFS);
                     tsk_error_set_errstr
                         ("hfs_cat_traverse: length of key %d in index node %d too large (%d vs %"
@@ -857,7 +847,6 @@ hfs_cat_traverse(HFS_INFO * hfs,
                     return 1;
                 }
 
-
                 /*
                    if (tsk_verbose)
                    tsk_fprintf(stderr,
@@ -956,19 +945,10 @@ hfs_cat_traverse(HFS_INFO * hfs,
                     return 1;
                 }
 
-                if (sizeof(hfs_btree_key_cat) > nodesize - rec_off) {
-                    tsk_error_set_errno(TSK_ERR_FS_GENFS);
-                    tsk_error_set_errstr
-                    ("hfs_cat_traverse: record %d in leaf node %d truncated",
-                        rec, cur_node);
-                    free(node);
-                    return 1;
-                }
-
                 key = (hfs_btree_key_cat *) & node[rec_off];
-
                 keylen = 2 + tsk_getu16(hfs->fs_info.endian, key->key_len);
-                if ((keylen) > nodesize) {
+
+                if (keylen > nodesize - rec_off) {
                     tsk_error_set_errno(TSK_ERR_FS_GENFS);
                     tsk_error_set_errstr
                         ("hfs_cat_traverse: length of key %d in leaf node %d too large (%d vs %"
diff --git a/tsk/fs/tsk_fs.h b/tsk/fs/tsk_fs.h
index 1921ef79b56c3c4482bde997650d4df713090fd0..2158584563104b5158975835cc73a11ccfdf243b 100644
--- a/tsk/fs/tsk_fs.h
+++ b/tsk/fs/tsk_fs.h
@@ -780,7 +780,15 @@ extern "C" {
 
     /**
     * Values for the file system type.  Each bit corresponds to a file
-    * system.
+    * system. The "[fs]_DETECT" value (such as TSK_FS_TYPE_NTSF_DETECT) is 
+    * the OR of all of the subtypes that 
+    * it could detect.  If there is only one type of that file system, 
+    * the [fs]_DETECT value will be the same as the type. 
+    *
+    * The _DETECT values should not be stored in TSK_FS_INFO.  Once 
+    * tsk_fs_open() has detected the type, it should assign the specific
+    * version in TSK_FS_INFO.
+    *
     */
     enum TSK_FS_TYPE_ENUM {
         TSK_FS_TYPE_DETECT = 0x00000000,        ///< Use autodetection methods
diff --git a/tsk/fs/yaffs.cpp b/tsk/fs/yaffs.cpp
index 68d8b0219b0bf596f663f7d63b7495269b0312ad..f5ec30a09dde02577111ed506242ec1c48b2fd2c 100755
--- a/tsk/fs/yaffs.cpp
+++ b/tsk/fs/yaffs.cpp
@@ -2439,7 +2439,7 @@ static uint8_t
     YAFFSFS_INFO *yfs = (YAFFSFS_INFO *)fs;
     char ls[12];
     YAFFSFS_PRINT_ADDR print;
-    char timeBuf[32];
+    char timeBuf[128];
     YaffsCacheObject * obj = NULL;
     YaffsCacheVersion * version = NULL;
     YaffsHeader * header = NULL;