Index: src/wp-includes/query.php
===================================================================
--- src/wp-includes/query.php	(revision 30299)
+++ src/wp-includes/query.php	(working copy)
@@ -3063,13 +3063,14 @@
 			if ( !$page )
 				$page = 1;
 
-			if ( empty($q['offset']) ) {
-				$pgstrt = absint( ( $page - 1 ) * $q['posts_per_page'] ) . ', ';
-			} else { // we're ignoring $page and using 'offset'
-				$q['offset'] = absint($q['offset']);
-				$pgstrt = $q['offset'] . ', ';
+			$pgstrt = ($page - 1) * $q['posts_per_page'];
+
+			if ( ! empty( $q['offset'] ) ) {
+				$q['offset'] = intval( $q['offset'] );
+				$pgstrt += $q['offset'];
+				$pgstrt = max( $pgstrt, 0 );
 			}
-			$limits = 'LIMIT ' . $pgstrt . $q['posts_per_page'];
+			$limits = 'LIMIT ' . $pgstrt . ', ' . $q['posts_per_page'];
 		}
 
 		// Comments feeds
Index: tests/phpunit/tests/query/results.php
===================================================================
--- tests/phpunit/tests/query/results.php	(revision 30299)
+++ tests/phpunit/tests/query/results.php	(working copy)
@@ -281,6 +281,27 @@
 		$this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
 	}
 
+	function test_query_negative_offset() {
+		$posts = $this->q->query('offset=-2');
+
+		// A standalone negative offset should have no effect
+		$expected = array (
+			0 => 'tags-a-and-c',
+			1 => 'tags-b-and-c',
+			2 => 'tags-a-and-b',
+			3 => 'tag-c',
+			4 => 'tag-b',
+			5 => 'tag-a',
+			6 => 'tags-a-b-c',
+			7 => 'raw-html-code',
+			8 => 'simple-markup-test',
+			9 => 'embedded-video',
+		);
+
+		$this->assertCount( 10, $posts );
+		$this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
+	}
+
 	function test_query_paged() {
 		$posts = $this->q->query('paged=2');
 
@@ -342,6 +363,59 @@
 	}
 
 	/**
+	 * @ticket 18897
+	 */
+	function test_query_negative_offset_and_paged() {
+		// Negative offset permits a posts_per_page on page 1 to be lower
+		// than posts_per_page on subsequent pages without skipping posts.
+		// e.g. posts_per_page = 5 on page 1; posts_per_page = 10 on page 2+
+		// For this test, post 6 through 15 should be returned.
+		$posts = $this->q->query('posts_per_page=10&paged=2&offset=-5');
+
+		$expected = array (
+			0 => 'tag-a',
+			1 => 'tags-a-b-c',
+			2 => 'raw-html-code',
+			3 => 'simple-markup-test',
+			4 => 'embedded-video',
+			5 => 'contributor-post-approved',
+			6 => 'one-comment',
+			7 => 'no-comments',
+			8 => 'many-trackbacks',
+			9 => 'one-trackback',
+		);
+
+		$this->assertCount( 10, $posts );
+		$this->assertTrue( $this->q->is_paged() );
+		$this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
+	}
+
+	/**
+	 * @ticket 18897
+	 */
+	function test_query_large_negative_offset_and_paged() {
+		$posts = $this->q->query('posts_per_page=10&paged=2&offset=-15');
+
+		// Offset cannot reach before starting index
+		$expected = array (
+			0 => 'tags-a-and-c',
+			1 => 'tags-b-and-c',
+			2 => 'tags-a-and-b',
+			3 => 'tag-c',
+			4 => 'tag-b',
+			5 => 'tag-a',
+			6 => 'tags-a-b-c',
+			7 => 'raw-html-code',
+			8 => 'simple-markup-test',
+			9 => 'embedded-video',
+		);
+
+		$this->assertCount( 10, $posts );
+		$this->assertTrue( $this->q->is_paged() );
+		$this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
+	}
+
+	/**
 	 * @ticket 11056
 	 */
 	function test_query_post_parent__in() {
